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.

191 lines
6.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. // Does this look like a shortcode?
  7. pub static ref SHORTCODE_RE: Regex = Regex::new(
  8. r#"\{(?:%|\{)\s+(\w+?)\((\w+?="?(?:.|\n)+?"?)?\)\s+(?:%|\})\}"#
  9. ).unwrap();
  10. // Parse the shortcode args with capture groups named after their type
  11. pub static ref SHORTCODE_ARGS_RE: Regex = Regex::new(
  12. r#"(?P<name>\w+)=\s*((?P<str>".*?")|(?P<float>[-+]?[0-9]+\.[0-9]+)|(?P<int>[-+]?[0-9]+)|(?P<bool>true|false))"#
  13. ).unwrap();
  14. }
  15. /// A shortcode that has a body
  16. /// Called by having some content like {% ... %} body {% end %}
  17. /// We need the struct to hold the data while we're processing the markdown
  18. #[derive(Debug)]
  19. pub struct ShortCode {
  20. name: String,
  21. args: HashMap<String, Value>,
  22. body: String,
  23. }
  24. impl ShortCode {
  25. pub fn new(name: &str, args: HashMap<String, Value>) -> ShortCode {
  26. ShortCode {
  27. name: name.to_string(),
  28. args,
  29. body: String::new(),
  30. }
  31. }
  32. pub fn append(&mut self, text: &str) {
  33. self.body.push_str(text)
  34. }
  35. pub fn render(&self, tera: &Tera) -> Result<String> {
  36. let mut context = Context::new();
  37. for (key, value) in &self.args {
  38. context.add(key, value);
  39. }
  40. context.add("body", &self.body);
  41. let tpl_name = format!("shortcodes/{}.html", self.name);
  42. tera.render(&tpl_name, &context)
  43. .chain_err(|| format!("Failed to render {} shortcode", self.name))
  44. }
  45. }
  46. /// Parse a shortcode without a body
  47. pub fn parse_shortcode(input: &str) -> (String, HashMap<String, Value>) {
  48. let mut args = HashMap::new();
  49. let caps = SHORTCODE_RE.captures(input).unwrap();
  50. // caps[0] is the full match
  51. let name = &caps[1];
  52. if let Some(arg_list) = caps.get(2) {
  53. for arg_cap in SHORTCODE_ARGS_RE.captures_iter(arg_list.as_str()) {
  54. let arg_name = arg_cap["name"].trim().to_string();
  55. if let Some(arg_val) = arg_cap.name("str") {
  56. args.insert(arg_name, to_value(arg_val.as_str().replace("\"", "")).unwrap());
  57. continue;
  58. }
  59. if let Some(arg_val) = arg_cap.name("int") {
  60. args.insert(arg_name, to_value(arg_val.as_str().parse::<i64>().unwrap()).unwrap());
  61. continue;
  62. }
  63. if let Some(arg_val) = arg_cap.name("float") {
  64. args.insert(arg_name, to_value(arg_val.as_str().parse::<f64>().unwrap()).unwrap());
  65. continue;
  66. }
  67. if let Some(arg_val) = arg_cap.name("bool") {
  68. args.insert(arg_name, to_value(arg_val.as_str() == "true").unwrap());
  69. continue;
  70. }
  71. }
  72. }
  73. (name.to_string(), args)
  74. }
  75. /// Renders a shortcode or return an error
  76. pub fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, Value>) -> Result<String> {
  77. let mut context = Context::new();
  78. for (key, value) in args.iter() {
  79. context.add(key, value);
  80. }
  81. let tpl_name = format!("shortcodes/{}.html", name);
  82. tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
  83. }
  84. #[cfg(test)]
  85. mod tests {
  86. use super::{parse_shortcode, SHORTCODE_RE};
  87. #[test]
  88. fn can_match_all_kinds_of_shortcode() {
  89. let inputs = vec![
  90. "{{ basic() }}",
  91. "{{ basic(ho=1) }}",
  92. "{{ basic(ho=\"hey\") }}",
  93. "{{ basic(ho=\"hey_underscore\") }}",
  94. "{{ basic(ho=\"hey-dash\") }}",
  95. "{% basic(ho=\"hey-dash\") %}",
  96. "{% basic(ho=\"hey_underscore\") %}",
  97. "{% basic() %}",
  98. "{% quo_te(author=\"Bob\") %}",
  99. "{{ quo_te(author=\"Bob\") }}",
  100. // https://github.com/Keats/gutenberg/issues/229
  101. r#"{{ youtube(id="dQw4w9WgXcQ",
  102. autoplay=true) }}"#,
  103. ];
  104. for i in inputs {
  105. println!("{}", i);
  106. assert!(SHORTCODE_RE.is_match(i));
  107. }
  108. }
  109. // https://github.com/Keats/gutenberg/issues/228
  110. #[test]
  111. fn doesnt_panic_on_invalid_shortcode() {
  112. let (name, args) = parse_shortcode(r#"{{ youtube(id="dQw4w9WgXcQ", autoplay) }}"#);
  113. assert_eq!(name, "youtube");
  114. assert_eq!(args["id"], "dQw4w9WgXcQ");
  115. assert!(args.get("autoplay").is_none());
  116. }
  117. #[test]
  118. fn can_parse_simple_shortcode_no_arg() {
  119. let (name, args) = parse_shortcode(r#"{{ basic() }}"#);
  120. assert_eq!(name, "basic");
  121. assert!(args.is_empty());
  122. }
  123. #[test]
  124. fn can_parse_simple_shortcode_one_arg() {
  125. let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#);
  126. assert_eq!(name, "youtube");
  127. assert_eq!(args["id"], "w7Ft2ymGmfc");
  128. }
  129. #[test]
  130. fn can_parse_simple_shortcode_several_arg() {
  131. let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#);
  132. assert_eq!(name, "youtube");
  133. assert_eq!(args["id"], "w7Ft2ymGmfc");
  134. assert_eq!(args["autoplay"], true);
  135. }
  136. #[test]
  137. fn can_parse_block_shortcode_several_arg() {
  138. let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#);
  139. assert_eq!(name, "youtube");
  140. assert_eq!(args["id"], "w7Ft2ymGmfc");
  141. assert_eq!(args["autoplay"], true);
  142. }
  143. #[test]
  144. fn can_parse_shortcode_number() {
  145. let (name, args) = parse_shortcode(r#"{% test(int=42, float=42.0, autoplay=false) %}"#);
  146. assert_eq!(name, "test");
  147. assert_eq!(args["int"], 42);
  148. assert_eq!(args["float"], 42.0);
  149. assert_eq!(args["autoplay"], false);
  150. }
  151. // https://github.com/Keats/gutenberg/issues/249
  152. #[test]
  153. fn can_parse_shortcode_with_comma_in_it() {
  154. let (name, args) = parse_shortcode(
  155. r#"{% quote(author="C++ Standard Core Language Defect Reports and Accepted Issues, Revision 82, delete and user-written deallocation function", href="http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#348") %}"#
  156. );
  157. assert_eq!(name, "quote");
  158. assert_eq!(args["author"], "C++ Standard Core Language Defect Reports and Accepted Issues, Revision 82, delete and user-written deallocation function");
  159. assert_eq!(args["href"], "http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#348");
  160. }
  161. }