use std::collections::HashMap; use regex::Regex; use tera::{Tera, Context, Value, to_value}; use errors::{Result, ResultExt}; lazy_static!{ // Does this look like a shortcode? pub static ref SHORTCODE_RE: Regex = Regex::new( r#"\{(?:%|\{)\s+(\w+?)\((\w+?="?(?:.|\n)+?"?)?\)\s+(?:%|\})\}"# ).unwrap(); // Parse the shortcode args with capture groups named after their type pub static ref SHORTCODE_ARGS_RE: Regex = Regex::new( r#"(?P\w+)=\s*((?P".*?")|(?P[-+]?[0-9]+\.[0-9]+)|(?P[-+]?[0-9]+)|(?Ptrue|false))"# ).unwrap(); } /// A shortcode that has a body /// Called by having some content like {% ... %} body {% end %} /// We need the struct to hold the data while we're processing the markdown #[derive(Debug)] pub struct ShortCode { name: String, args: HashMap, body: String, } impl ShortCode { pub fn new(name: &str, args: HashMap) -> ShortCode { ShortCode { name: name.to_string(), args, body: String::new(), } } pub fn append(&mut self, text: &str) { self.body.push_str(text) } pub fn render(&self, tera: &Tera) -> Result { let mut context = Context::new(); for (key, value) in &self.args { context.add(key, value); } context.add("body", &self.body); let tpl_name = format!("shortcodes/{}.html", self.name); tera.render(&tpl_name, &context) .chain_err(|| format!("Failed to render {} shortcode", self.name)) } } /// Parse a shortcode without a body pub fn parse_shortcode(input: &str) -> (String, HashMap) { let mut args = HashMap::new(); let caps = SHORTCODE_RE.captures(input).unwrap(); // caps[0] is the full match let name = &caps[1]; if let Some(arg_list) = caps.get(2) { for arg_cap in SHORTCODE_ARGS_RE.captures_iter(arg_list.as_str()) { let arg_name = arg_cap["name"].trim().to_string(); if let Some(arg_val) = arg_cap.name("str") { args.insert(arg_name, to_value(arg_val.as_str().replace("\"", "")).unwrap()); continue; } if let Some(arg_val) = arg_cap.name("int") { args.insert(arg_name, to_value(arg_val.as_str().parse::().unwrap()).unwrap()); continue; } if let Some(arg_val) = arg_cap.name("float") { args.insert(arg_name, to_value(arg_val.as_str().parse::().unwrap()).unwrap()); continue; } if let Some(arg_val) = arg_cap.name("bool") { args.insert(arg_name, to_value(arg_val.as_str() == "true").unwrap()); continue; } } } (name.to_string(), args) } /// Renders a shortcode or return an error pub fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap) -> Result { let mut context = Context::new(); for (key, value) in args.iter() { context.add(key, value); } let tpl_name = format!("shortcodes/{}.html", name); tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name)) } #[cfg(test)] mod tests { use super::{parse_shortcode, SHORTCODE_RE}; #[test] fn can_match_all_kinds_of_shortcode() { let inputs = vec![ "{{ basic() }}", "{{ basic(ho=1) }}", "{{ basic(ho=\"hey\") }}", "{{ basic(ho=\"hey_underscore\") }}", "{{ basic(ho=\"hey-dash\") }}", "{% basic(ho=\"hey-dash\") %}", "{% basic(ho=\"hey_underscore\") %}", "{% basic() %}", "{% quo_te(author=\"Bob\") %}", "{{ quo_te(author=\"Bob\") }}", // https://github.com/Keats/gutenberg/issues/229 r#"{{ youtube(id="dQw4w9WgXcQ", autoplay=true) }}"#, ]; for i in inputs { println!("{}", i); assert!(SHORTCODE_RE.is_match(i)); } } // https://github.com/Keats/gutenberg/issues/228 #[test] fn doesnt_panic_on_invalid_shortcode() { let (name, args) = parse_shortcode(r#"{{ youtube(id="dQw4w9WgXcQ", autoplay) }}"#); assert_eq!(name, "youtube"); assert_eq!(args["id"], "dQw4w9WgXcQ"); assert!(args.get("autoplay").is_none()); } #[test] fn can_parse_simple_shortcode_no_arg() { let (name, args) = parse_shortcode(r#"{{ basic() }}"#); assert_eq!(name, "basic"); assert!(args.is_empty()); } #[test] fn can_parse_simple_shortcode_one_arg() { let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#); assert_eq!(name, "youtube"); assert_eq!(args["id"], "w7Ft2ymGmfc"); } #[test] fn can_parse_simple_shortcode_several_arg() { let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#); assert_eq!(name, "youtube"); assert_eq!(args["id"], "w7Ft2ymGmfc"); assert_eq!(args["autoplay"], true); } #[test] fn can_parse_block_shortcode_several_arg() { let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#); assert_eq!(name, "youtube"); assert_eq!(args["id"], "w7Ft2ymGmfc"); assert_eq!(args["autoplay"], true); } #[test] fn can_parse_shortcode_number() { let (name, args) = parse_shortcode(r#"{% test(int=42, float=42.0, autoplay=false) %}"#); assert_eq!(name, "test"); assert_eq!(args["int"], 42); assert_eq!(args["float"], 42.0); assert_eq!(args["autoplay"], false); } // https://github.com/Keats/gutenberg/issues/249 #[test] fn can_parse_shortcode_with_comma_in_it() { let (name, args) = parse_shortcode( 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") %}"# ); assert_eq!(name, "quote"); assert_eq!(args["author"], "C++ Standard Core Language Defect Reports and Accepted Issues, Revision 82, delete and user-written deallocation function"); assert_eq!(args["href"], "http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#348"); } }