You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
5.3KB

  1. use std::collections::HashMap;
  2. use regex::Regex;
  3. use tera::{Tera, Context, Value, to_value};
  4. use errors::{Result, ResultExt};
  5. lazy_static!{
  6. pub static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:word:]]+?)\(([[:word:]]+?="?.+?"?)?\)\s+(?:%|\})\}"#).unwrap();
  7. }
  8. /// A shortcode that has a body
  9. /// Called by having some content like {% ... %} body {% end %}
  10. /// We need the struct to hold the data while we're processing the markdown
  11. #[derive(Debug)]
  12. pub struct ShortCode {
  13. name: String,
  14. args: HashMap<String, Value>,
  15. body: String,
  16. }
  17. impl ShortCode {
  18. pub fn new(name: &str, args: HashMap<String, Value>) -> ShortCode {
  19. ShortCode {
  20. name: name.to_string(),
  21. args,
  22. body: String::new(),
  23. }
  24. }
  25. pub fn append(&mut self, text: &str) {
  26. self.body.push_str(text)
  27. }
  28. pub fn render(&self, tera: &Tera) -> Result<String> {
  29. let mut context = Context::new();
  30. for (key, value) in &self.args {
  31. context.add(key, value);
  32. }
  33. context.add("body", &self.body);
  34. let tpl_name = format!("shortcodes/{}.html", self.name);
  35. tera.render(&tpl_name, &context)
  36. .chain_err(|| format!("Failed to render {} shortcode", self.name))
  37. }
  38. }
  39. /// Parse a shortcode without a body
  40. pub fn parse_shortcode(input: &str) -> (String, HashMap<String, Value>) {
  41. let mut args = HashMap::new();
  42. let caps = SHORTCODE_RE.captures(input).unwrap();
  43. // caps[0] is the full match
  44. let name = &caps[1];
  45. if let Some(arg_list) = caps.get(2) {
  46. for arg in arg_list.as_str().split(',') {
  47. let bits = arg.split('=').collect::<Vec<_>>();
  48. let arg_name = bits[0].trim().to_string();
  49. let arg_val = bits[1].replace("\"", "");
  50. // Regex captures will be str so we need to figure out if they are
  51. // actually str or bool/number
  52. if input.contains(&format!("{}=\"{}\"", arg_name, arg_val)) {
  53. // that's a str, just add it
  54. args.insert(arg_name, to_value(arg_val).unwrap());
  55. continue;
  56. }
  57. if input.contains(&format!("{}=true", arg_name)) {
  58. args.insert(arg_name, to_value(true).unwrap());
  59. continue;
  60. }
  61. if input.contains(&format!("{}=false", arg_name)) {
  62. args.insert(arg_name, to_value(false).unwrap());
  63. continue;
  64. }
  65. // Not a string or a bool, a number then?
  66. if arg_val.contains('.') {
  67. if let Ok(float) = arg_val.parse::<f64>() {
  68. args.insert(arg_name, to_value(float).unwrap());
  69. }
  70. continue;
  71. }
  72. // must be an integer
  73. if let Ok(int) = arg_val.parse::<i64>() {
  74. args.insert(arg_name, to_value(int).unwrap());
  75. }
  76. }
  77. }
  78. (name.to_string(), args)
  79. }
  80. /// Renders a shortcode or return an error
  81. pub fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, Value>) -> Result<String> {
  82. let mut context = Context::new();
  83. for (key, value) in args.iter() {
  84. context.add(key, value);
  85. }
  86. let tpl_name = format!("shortcodes/{}.html", name);
  87. tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
  88. }
  89. #[cfg(test)]
  90. mod tests {
  91. use super::{parse_shortcode, SHORTCODE_RE};
  92. #[test]
  93. fn can_match_all_kinds_of_shortcode() {
  94. let inputs = vec![
  95. "{{ basic() }}",
  96. "{{ basic(ho=1) }}",
  97. "{{ basic(ho=\"hey\") }}",
  98. "{{ basic(ho=\"hey_underscore\") }}",
  99. "{{ basic(ho=\"hey-dash\") }}",
  100. "{% basic(ho=\"hey-dash\") %}",
  101. "{% basic(ho=\"hey_underscore\") %}",
  102. "{% basic() %}",
  103. "{% quo_te(author=\"Bob\") %}",
  104. "{{ quo_te(author=\"Bob\") }}",
  105. ];
  106. for i in inputs {
  107. println!("{}", i);
  108. assert!(SHORTCODE_RE.is_match(i));
  109. }
  110. }
  111. #[test]
  112. fn can_parse_simple_shortcode_no_arg() {
  113. let (name, args) = parse_shortcode(r#"{{ basic() }}"#);
  114. assert_eq!(name, "basic");
  115. assert!(args.is_empty());
  116. }
  117. #[test]
  118. fn can_parse_simple_shortcode_one_arg() {
  119. let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#);
  120. assert_eq!(name, "youtube");
  121. assert_eq!(args["id"], "w7Ft2ymGmfc");
  122. }
  123. #[test]
  124. fn can_parse_simple_shortcode_several_arg() {
  125. let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#);
  126. assert_eq!(name, "youtube");
  127. assert_eq!(args["id"], "w7Ft2ymGmfc");
  128. assert_eq!(args["autoplay"], true);
  129. }
  130. #[test]
  131. fn can_parse_block_shortcode_several_arg() {
  132. let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#);
  133. assert_eq!(name, "youtube");
  134. assert_eq!(args["id"], "w7Ft2ymGmfc");
  135. assert_eq!(args["autoplay"], true);
  136. }
  137. #[test]
  138. fn can_parse_shortcode_number() {
  139. let (name, args) = parse_shortcode(r#"{% test(int=42, float=42.0, autoplay=true) %}"#);
  140. assert_eq!(name, "test");
  141. assert_eq!(args["int"], 42);
  142. assert_eq!(args["float"], 42.0);
  143. assert_eq!(args["autoplay"], true);
  144. }
  145. }