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.

shortcode.rs 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. use pest::Parser;
  2. use pest::iterators::Pair;
  3. use tera::{Tera, Map, Context, Value, to_value};
  4. use errors::{Result, ResultExt};
  5. use config::Config;
  6. // This include forces recompiling this source file if the grammar file changes.
  7. // Uncomment it when doing changes to the .pest file
  8. // const _GRAMMAR: &str = include_str!("content.pest");
  9. #[derive(Parser)]
  10. #[grammar = "content.pest"]
  11. pub struct ContentParser;
  12. fn replace_string_markers(input: &str) -> String {
  13. match input.chars().next().unwrap() {
  14. '"' => input.replace('"', "").to_string(),
  15. '\'' => input.replace('\'', "").to_string(),
  16. '`' => input.replace('`', "").to_string(),
  17. _ => unreachable!("How did you even get there"),
  18. }
  19. }
  20. fn parse_literal(pair: Pair<Rule>) -> Value {
  21. let mut val = None;
  22. for p in pair.into_inner() {
  23. match p.as_rule() {
  24. Rule::boolean => match p.as_str() {
  25. "true" => val = Some(Value::Bool(true)),
  26. "false" => val = Some(Value::Bool(false)),
  27. _ => unreachable!(),
  28. },
  29. Rule::string => val = Some(Value::String(replace_string_markers(p.as_str()))),
  30. Rule::float => {
  31. val = Some(to_value(p.as_str().parse::<f64>().unwrap()).unwrap());
  32. },
  33. Rule::int => {
  34. val = Some(to_value(p.as_str().parse::<i64>().unwrap()).unwrap());
  35. },
  36. _ => unreachable!("Unknown literal: {:?}", p)
  37. };
  38. }
  39. val.unwrap()
  40. }
  41. /// Returns (shortcode_name, kwargs)
  42. fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) {
  43. let mut name = None;
  44. let mut args = Map::new();
  45. for p in pair.into_inner() {
  46. match p.as_rule() {
  47. Rule::ident => { name = Some(p.into_span().as_str().to_string()); },
  48. Rule::kwarg => {
  49. let mut arg_name = None;
  50. let mut arg_val = None;
  51. for p2 in p.into_inner() {
  52. match p2.as_rule() {
  53. Rule::ident => { arg_name = Some(p2.into_span().as_str().to_string()); },
  54. Rule::literal => { arg_val = Some(parse_literal(p2)); },
  55. Rule::array => {
  56. let mut vals = vec![];
  57. for p3 in p2.into_inner() {
  58. match p3.as_rule() {
  59. Rule::literal => vals.push(parse_literal(p3)),
  60. _ => unreachable!("Got something other than literal in an array: {:?}", p3),
  61. }
  62. }
  63. arg_val = Some(Value::Array(vals));
  64. },
  65. _ => unreachable!("Got something unexpected in a kwarg: {:?}", p2),
  66. }
  67. }
  68. args.insert(arg_name.unwrap(), arg_val.unwrap());
  69. },
  70. _ => unreachable!("Got something unexpected in a shortcode: {:?}", p)
  71. }
  72. }
  73. (name.unwrap(), args)
  74. }
  75. fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config: &Config, body: Option<&str>) -> Result<String> {
  76. let mut context = Context::new();
  77. for (key, value) in args.iter() {
  78. context.insert(key, value);
  79. }
  80. if let Some(ref b) = body {
  81. // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line
  82. context.insert("body", b.trim_right());
  83. }
  84. context.insert("config", config);
  85. let tpl_name = format!("shortcodes/{}.html", name);
  86. tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
  87. }
  88. pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result<String> {
  89. let mut res = String::with_capacity(content.len());
  90. let mut pairs = match ContentParser::parse(Rule::page, content) {
  91. Ok(p) => p,
  92. Err(e) => {
  93. let fancy_e = e.renamed_rules(|rule| {
  94. match *rule {
  95. Rule::int => "an integer".to_string(),
  96. Rule::float => "a float".to_string(),
  97. Rule::string => "a string".to_string(),
  98. Rule::literal => "a literal (int, float, string, bool)".to_string(),
  99. Rule::array => "an array".to_string(),
  100. Rule::kwarg => "a keyword argument".to_string(),
  101. Rule::ident => "an identifier".to_string(),
  102. _ => format!("TODO error: {:?}", rule).to_string(),
  103. }
  104. });
  105. bail!("{}", fancy_e);
  106. },
  107. };
  108. // We have at least a `page` pair
  109. for p in pairs.next().unwrap().into_inner() {
  110. match p.as_rule() {
  111. Rule::text | Rule::text_in_ignored_body_sc | Rule::text_in_body_sc => res.push_str(p.into_span().as_str()),
  112. Rule::inline_shortcode => {
  113. let (name, args) = parse_shortcode_call(p);
  114. res.push_str(&render_shortcode(name, args, tera, config, None)?);
  115. },
  116. Rule::shortcode_with_body => {
  117. let mut inner = p.into_inner();
  118. // 3 items in inner: call, body, end
  119. // we don't care about the closing tag
  120. let (name, args) = parse_shortcode_call(inner.next().unwrap());
  121. let body = inner.next().unwrap().into_span().as_str();
  122. res.push_str(&render_shortcode(name, args, tera, config, Some(body))?);
  123. },
  124. Rule::ignored_inline_shortcode => {
  125. res.push_str(
  126. &p.into_span().as_str()
  127. .replacen("{{/*", "{{", 1)
  128. .replacen("*/}}", "}}", 1)
  129. );
  130. },
  131. Rule::ignored_shortcode_with_body => {
  132. for p2 in p.into_inner() {
  133. match p2.as_rule() {
  134. Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => {
  135. res.push_str(
  136. &p2.into_span().as_str()
  137. .replacen("{%/*", "{%", 1)
  138. .replacen("*/%}", "%}", 1)
  139. );
  140. },
  141. Rule::text_in_ignored_body_sc => res.push_str(p2.into_span().as_str()),
  142. _ => unreachable!("Got something weird in an ignored shortcode"),
  143. }
  144. }
  145. },
  146. _ => unreachable!("unexpected page rule: {:?}", p.as_rule()),
  147. }
  148. }
  149. Ok(res)
  150. }
  151. #[cfg(test)]
  152. mod tests {
  153. use super::*;
  154. macro_rules! assert_lex_rule {
  155. ($rule: expr, $input: expr) => {
  156. let res = ContentParser::parse($rule, $input);
  157. println!("{:?}", $input);
  158. println!("{:#?}", res);
  159. if res.is_err() {
  160. println!("{}", res.unwrap_err());
  161. panic!();
  162. }
  163. assert!(res.is_ok());
  164. assert_eq!(res.unwrap().last().unwrap().into_span().end(), $input.len());
  165. };
  166. }
  167. #[test]
  168. fn lex_text() {
  169. let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"];
  170. for i in inputs {
  171. assert_lex_rule!(Rule::text, i);
  172. }
  173. }
  174. #[test]
  175. fn lex_inline_shortcode() {
  176. let inputs = vec![
  177. "{{ youtube() }}",
  178. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  179. "{{ youtube(id=1, \nautoplay=true, url='hey') }}",
  180. ];
  181. for i in inputs {
  182. assert_lex_rule!(Rule::inline_shortcode, i);
  183. }
  184. }
  185. #[test]
  186. fn lex_inline_ignored_shortcode() {
  187. let inputs = vec![
  188. "{{/* youtube() */}}",
  189. "{{/* youtube(id=1, autoplay=true, url='hey') */}}",
  190. "{{/* youtube(id=1, \nautoplay=true, \nurl='hey') */}}",
  191. ];
  192. for i in inputs {
  193. assert_lex_rule!(Rule::ignored_inline_shortcode, i);
  194. }
  195. }
  196. #[test]
  197. fn lex_shortcode_with_body() {
  198. let inputs = vec![
  199. r#"{% youtube() %}
  200. Some text
  201. {% end %}"#,
  202. r#"{% youtube(id=1,
  203. autoplay=true, url='hey') %}
  204. Some text
  205. {% end %}"#,
  206. ];
  207. for i in inputs {
  208. assert_lex_rule!(Rule::shortcode_with_body, i);
  209. }
  210. }
  211. #[test]
  212. fn lex_ignored_shortcode_with_body() {
  213. let inputs = vec![
  214. r#"{%/* youtube() */%}
  215. Some text
  216. {%/* end */%}"#,
  217. r#"{%/* youtube(id=1,
  218. autoplay=true, url='hey') */%}
  219. Some text
  220. {%/* end */%}"#,
  221. ];
  222. for i in inputs {
  223. assert_lex_rule!(Rule::ignored_shortcode_with_body, i);
  224. }
  225. }
  226. #[test]
  227. fn lex_page() {
  228. let inputs = vec![
  229. "Some text and a shortcode `{{/* youtube() */}}`",
  230. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  231. "{{ youtube(id=1, \nautoplay=true, url='hey') }} that's it",
  232. r#"
  233. This is a test
  234. {% hello() %}
  235. Body {{ var }}
  236. {% end %}
  237. "#
  238. ];
  239. for i in inputs {
  240. assert_lex_rule!(Rule::page, i);
  241. }
  242. }
  243. #[test]
  244. fn does_nothing_with_no_shortcodes() {
  245. let res = render_shortcodes("Hello World", &Tera::default(), &Config::default());
  246. assert_eq!(res.unwrap(), "Hello World");
  247. }
  248. #[test]
  249. fn can_unignore_inline_shortcode() {
  250. let res = render_shortcodes(
  251. "Hello World {{/* youtube() */}}",
  252. &Tera::default(),
  253. &Config::default(),
  254. );
  255. assert_eq!(res.unwrap(), "Hello World {{ youtube() }}");
  256. }
  257. #[test]
  258. fn can_unignore_shortcode_with_body() {
  259. let res = render_shortcodes(r#"
  260. Hello World
  261. {%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default(), &Config::default());
  262. assert_eq!(res.unwrap(), "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}");
  263. }
  264. #[test]
  265. fn can_parse_shortcode_arguments() {
  266. let inputs = vec![
  267. ("{{ youtube() }}", "youtube", Map::new()),
  268. (
  269. "{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}",
  270. "youtube",
  271. {
  272. let mut m = Map::new();
  273. m.insert("id".to_string(), to_value(1).unwrap());
  274. m.insert("autoplay".to_string(), to_value(true).unwrap());
  275. m.insert("hello".to_string(), to_value("salut").unwrap());
  276. m.insert("float".to_string(), to_value(1.2).unwrap());
  277. m
  278. }
  279. ),
  280. (
  281. "{{ gallery(photos=['something', 'else'], fullscreen=true) }}",
  282. "gallery",
  283. {
  284. let mut m = Map::new();
  285. m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
  286. m.insert("fullscreen".to_string(), to_value(true).unwrap());
  287. m
  288. }
  289. ),
  290. ];
  291. for (i, n, a) in inputs {
  292. let mut res = ContentParser::parse(Rule::inline_shortcode, i).unwrap();
  293. let (name, args) = parse_shortcode_call(res.next().unwrap());
  294. assert_eq!(name, n);
  295. assert_eq!(args, a);
  296. }
  297. }
  298. #[test]
  299. fn can_render_inline_shortcodes() {
  300. let mut tera = Tera::default();
  301. tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap();
  302. let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera, &Config::default()).unwrap();
  303. assert_eq!(res, "Inline Hello 1.");
  304. }
  305. #[test]
  306. fn can_render_shortcodes_with_body() {
  307. let mut tera = Tera::default();
  308. tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap();
  309. let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap();
  310. assert_eq!(res, "Body\n Hey!");
  311. }
  312. #[test]
  313. fn errors_on_unterminated_shortcode() {
  314. let res = render_shortcodes("{{ youtube(", &Tera::default(), &Config::default());
  315. assert!(res.is_err());
  316. }
  317. }