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 16KB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. use pest::iterators::Pair;
  2. use pest::Parser;
  3. use regex::Regex;
  4. use tera::{to_value, Context, Map, Value};
  5. use context::RenderContext;
  6. use errors::{Error, Result};
  7. // This include forces recompiling this source file if the grammar file changes.
  8. // Uncomment it when doing changes to the .pest file
  9. const _GRAMMAR: &str = include_str!("content.pest");
  10. #[derive(Parser)]
  11. #[grammar = "content.pest"]
  12. pub struct ContentParser;
  13. lazy_static! {
  14. static ref MULTIPLE_NEWLINE_RE: Regex = Regex::new(r"\n\s*\n").unwrap();
  15. static ref OUTER_NEWLINE_RE: Regex = Regex::new(r"^\s*\n|\n\s*$").unwrap();
  16. }
  17. fn replace_string_markers(input: &str) -> String {
  18. match input.chars().next().unwrap() {
  19. '"' => input.replace('"', ""),
  20. '\'' => input.replace('\'', ""),
  21. '`' => input.replace('`', ""),
  22. _ => unreachable!("How did you even get there"),
  23. }
  24. }
  25. fn parse_literal(pair: Pair<Rule>) -> Value {
  26. let mut val = None;
  27. for p in pair.into_inner() {
  28. match p.as_rule() {
  29. Rule::boolean => match p.as_str() {
  30. "true" => val = Some(Value::Bool(true)),
  31. "false" => val = Some(Value::Bool(false)),
  32. _ => unreachable!(),
  33. },
  34. Rule::string => val = Some(Value::String(replace_string_markers(p.as_str()))),
  35. Rule::float => {
  36. val = Some(to_value(p.as_str().parse::<f64>().unwrap()).unwrap());
  37. }
  38. Rule::int => {
  39. val = Some(to_value(p.as_str().parse::<i64>().unwrap()).unwrap());
  40. }
  41. _ => unreachable!("Unknown literal: {:?}", p),
  42. };
  43. }
  44. val.unwrap()
  45. }
  46. /// Returns (shortcode_name, kwargs)
  47. fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) {
  48. let mut name = None;
  49. let mut args = Map::new();
  50. for p in pair.into_inner() {
  51. match p.as_rule() {
  52. Rule::ident => {
  53. name = Some(p.as_span().as_str().to_string());
  54. }
  55. Rule::kwarg => {
  56. let mut arg_name = None;
  57. let mut arg_val = None;
  58. for p2 in p.into_inner() {
  59. match p2.as_rule() {
  60. Rule::ident => {
  61. arg_name = Some(p2.as_span().as_str().to_string());
  62. }
  63. Rule::literal => {
  64. arg_val = Some(parse_literal(p2));
  65. }
  66. Rule::array => {
  67. let mut vals = vec![];
  68. for p3 in p2.into_inner() {
  69. match p3.as_rule() {
  70. Rule::literal => vals.push(parse_literal(p3)),
  71. _ => unreachable!(
  72. "Got something other than literal in an array: {:?}",
  73. p3
  74. ),
  75. }
  76. }
  77. arg_val = Some(Value::Array(vals));
  78. }
  79. _ => unreachable!("Got something unexpected in a kwarg: {:?}", p2),
  80. }
  81. }
  82. args.insert(arg_name.unwrap(), arg_val.unwrap());
  83. }
  84. _ => unreachable!("Got something unexpected in a shortcode: {:?}", p),
  85. }
  86. }
  87. (name.unwrap(), args)
  88. }
  89. fn render_shortcode(
  90. name: &str,
  91. args: &Map<String, Value>,
  92. context: &RenderContext,
  93. body: Option<&str>,
  94. ) -> Result<String> {
  95. let mut tera_context = Context::new();
  96. for (key, value) in args.iter() {
  97. tera_context.insert(key, value);
  98. }
  99. if let Some(ref b) = body {
  100. // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line
  101. tera_context.insert("body", b.trim_end());
  102. }
  103. tera_context.extend(context.tera_context.clone());
  104. let template_name = format!("shortcodes/{}.html", name);
  105. let res = utils::templates::render_template(&template_name, &context.tera, tera_context, &None)
  106. .map_err(|e| Error::chain(format!("Failed to render {} shortcode", name), e))?;
  107. // Small hack to avoid having multiple blank lines because of Tera tags for example
  108. // A blank like will cause the markdown parser to think we're out of HTML and start looking
  109. // at indentation, making the output a code block.
  110. let res = MULTIPLE_NEWLINE_RE.replace_all(&res, "\n");
  111. let res = OUTER_NEWLINE_RE.replace_all(&res, "");
  112. Ok(res.to_string())
  113. }
  114. pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
  115. let mut res = String::with_capacity(content.len());
  116. let mut pairs = match ContentParser::parse(Rule::page, content) {
  117. Ok(p) => p,
  118. Err(e) => {
  119. let fancy_e = e.renamed_rules(|rule| match *rule {
  120. Rule::int => "an integer".to_string(),
  121. Rule::float => "a float".to_string(),
  122. Rule::string => "a string".to_string(),
  123. Rule::literal => "a literal (int, float, string, bool)".to_string(),
  124. Rule::array => "an array".to_string(),
  125. Rule::kwarg => "a keyword argument".to_string(),
  126. Rule::ident => "an identifier".to_string(),
  127. Rule::inline_shortcode => "an inline shortcode".to_string(),
  128. Rule::ignored_inline_shortcode => "an ignored inline shortcode".to_string(),
  129. Rule::sc_body_start => "the start of a shortcode".to_string(),
  130. Rule::ignored_sc_body_start => "the start of an ignored shortcode".to_string(),
  131. Rule::text => "some text".to_string(),
  132. Rule::EOI => "end of input".to_string(),
  133. Rule::double_quoted_string => "double quoted string".to_string(),
  134. Rule::single_quoted_string => "single quoted string".to_string(),
  135. Rule::backquoted_quoted_string => "backquoted quoted string".to_string(),
  136. Rule::boolean => "a boolean (true, false)".to_string(),
  137. Rule::all_chars => "a alphanumerical character".to_string(),
  138. Rule::kwargs => "a list of keyword arguments".to_string(),
  139. Rule::sc_def => "a shortcode definition".to_string(),
  140. Rule::shortcode_with_body => "a shortcode with body".to_string(),
  141. Rule::ignored_shortcode_with_body => "an ignored shortcode with body".to_string(),
  142. Rule::sc_body_end => "{% end %}".to_string(),
  143. Rule::ignored_sc_body_end => "{%/* end */%}".to_string(),
  144. Rule::text_in_body_sc => "text in a shortcode body".to_string(),
  145. Rule::text_in_ignored_body_sc => "text in an ignored shortcode body".to_string(),
  146. Rule::content => "some content".to_string(),
  147. Rule::page => "a page".to_string(),
  148. Rule::WHITESPACE => "whitespace".to_string(),
  149. });
  150. bail!("{}", fancy_e);
  151. }
  152. };
  153. // We have at least a `page` pair
  154. for p in pairs.next().unwrap().into_inner() {
  155. match p.as_rule() {
  156. Rule::text => res.push_str(p.as_span().as_str()),
  157. Rule::inline_shortcode => {
  158. let (name, args) = parse_shortcode_call(p);
  159. res.push_str(&render_shortcode(&name, &args, context, None)?);
  160. }
  161. Rule::shortcode_with_body => {
  162. let mut inner = p.into_inner();
  163. // 3 items in inner: call, body, end
  164. // we don't care about the closing tag
  165. let (name, args) = parse_shortcode_call(inner.next().unwrap());
  166. let body = inner.next().unwrap().as_span().as_str();
  167. res.push_str(&render_shortcode(&name, &args, context, Some(body))?);
  168. }
  169. Rule::ignored_inline_shortcode => {
  170. res.push_str(
  171. &p.as_span().as_str().replacen("{{/*", "{{", 1).replacen("*/}}", "}}", 1),
  172. );
  173. }
  174. Rule::ignored_shortcode_with_body => {
  175. for p2 in p.into_inner() {
  176. match p2.as_rule() {
  177. Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => {
  178. res.push_str(
  179. &p2.as_span()
  180. .as_str()
  181. .replacen("{%/*", "{%", 1)
  182. .replacen("*/%}", "%}", 1),
  183. );
  184. }
  185. Rule::text_in_ignored_body_sc => res.push_str(p2.as_span().as_str()),
  186. _ => unreachable!("Got something weird in an ignored shortcode: {:?}", p2),
  187. }
  188. }
  189. }
  190. Rule::EOI => (),
  191. _ => unreachable!("unexpected page rule: {:?}", p.as_rule()),
  192. }
  193. }
  194. Ok(res)
  195. }
  196. #[cfg(test)]
  197. mod tests {
  198. use std::collections::HashMap;
  199. use super::*;
  200. use config::Config;
  201. use front_matter::InsertAnchor;
  202. use tera::Tera;
  203. macro_rules! assert_lex_rule {
  204. ($rule: expr, $input: expr) => {
  205. let res = ContentParser::parse($rule, $input);
  206. println!("{:?}", $input);
  207. println!("{:#?}", res);
  208. if res.is_err() {
  209. println!("{}", res.unwrap_err());
  210. panic!();
  211. }
  212. assert!(res.is_ok());
  213. assert_eq!(res.unwrap().last().unwrap().as_span().end(), $input.len());
  214. };
  215. }
  216. fn render_shortcodes(code: &str, tera: &Tera) -> String {
  217. let config = Config::default();
  218. let permalinks = HashMap::new();
  219. let context = RenderContext::new(&tera, &config, "", &permalinks, InsertAnchor::None);
  220. super::render_shortcodes(code, &context).unwrap()
  221. }
  222. #[test]
  223. fn lex_text() {
  224. let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"];
  225. for i in inputs {
  226. assert_lex_rule!(Rule::text, i);
  227. }
  228. }
  229. #[test]
  230. fn lex_inline_shortcode() {
  231. let inputs = vec![
  232. "{{ youtube() }}",
  233. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  234. "{{ youtube(id=1, \nautoplay=true, url='hey') }}",
  235. ];
  236. for i in inputs {
  237. assert_lex_rule!(Rule::inline_shortcode, i);
  238. }
  239. }
  240. #[test]
  241. fn lex_inline_ignored_shortcode() {
  242. let inputs = vec![
  243. "{{/* youtube() */}}",
  244. "{{/* youtube(id=1, autoplay=true, url='hey') */}}",
  245. "{{/* youtube(id=1, \nautoplay=true, \nurl='hey') */}}",
  246. ];
  247. for i in inputs {
  248. assert_lex_rule!(Rule::ignored_inline_shortcode, i);
  249. }
  250. }
  251. #[test]
  252. fn lex_shortcode_with_body() {
  253. let inputs = vec![
  254. r#"{% youtube() %}
  255. Some text
  256. {% end %}"#,
  257. r#"{% youtube(id=1,
  258. autoplay=true, url='hey') %}
  259. Some text
  260. {% end %}"#,
  261. ];
  262. for i in inputs {
  263. assert_lex_rule!(Rule::shortcode_with_body, i);
  264. }
  265. }
  266. #[test]
  267. fn lex_ignored_shortcode_with_body() {
  268. let inputs = vec![
  269. r#"{%/* youtube() */%}
  270. Some text
  271. {%/* end */%}"#,
  272. r#"{%/* youtube(id=1,
  273. autoplay=true, url='hey') */%}
  274. Some text
  275. {%/* end */%}"#,
  276. ];
  277. for i in inputs {
  278. assert_lex_rule!(Rule::ignored_shortcode_with_body, i);
  279. }
  280. }
  281. #[test]
  282. fn lex_page() {
  283. let inputs = vec![
  284. "Some text and a shortcode `{{/* youtube() */}}`",
  285. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  286. "{{ youtube(id=1, \nautoplay=true, url='hey') }} that's it",
  287. r#"
  288. This is a test
  289. {% hello() %}
  290. Body {{ var }}
  291. {% end %}
  292. "#,
  293. ];
  294. for i in inputs {
  295. assert_lex_rule!(Rule::page, i);
  296. }
  297. }
  298. #[test]
  299. fn does_nothing_with_no_shortcodes() {
  300. let res = render_shortcodes("Hello World", &Tera::default());
  301. assert_eq!(res, "Hello World");
  302. }
  303. #[test]
  304. fn can_unignore_inline_shortcode() {
  305. let res = render_shortcodes("Hello World {{/* youtube() */}}", &Tera::default());
  306. assert_eq!(res, "Hello World {{ youtube() }}");
  307. }
  308. #[test]
  309. fn can_unignore_shortcode_with_body() {
  310. let res = render_shortcodes(
  311. r#"
  312. Hello World
  313. {%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#,
  314. &Tera::default(),
  315. );
  316. assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}");
  317. }
  318. // https://github.com/Keats/gutenberg/issues/383
  319. #[test]
  320. fn unignore_shortcode_with_body_does_not_swallow_initial_whitespace() {
  321. let res = render_shortcodes(
  322. r#"
  323. Hello World
  324. {%/* youtube() */%}
  325. Some body {{ hello() }}{%/* end */%}"#,
  326. &Tera::default(),
  327. );
  328. assert_eq!(res, "\nHello World\n{% youtube() %}\nSome body {{ hello() }}{% end %}");
  329. }
  330. #[test]
  331. fn can_parse_shortcode_arguments() {
  332. let inputs = vec![
  333. ("{{ youtube() }}", "youtube", Map::new()),
  334. ("{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}", "youtube", {
  335. let mut m = Map::new();
  336. m.insert("id".to_string(), to_value(1).unwrap());
  337. m.insert("autoplay".to_string(), to_value(true).unwrap());
  338. m.insert("hello".to_string(), to_value("salut").unwrap());
  339. m.insert("float".to_string(), to_value(1.2).unwrap());
  340. m
  341. }),
  342. ("{{ gallery(photos=['something', 'else'], fullscreen=true) }}", "gallery", {
  343. let mut m = Map::new();
  344. m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
  345. m.insert("fullscreen".to_string(), to_value(true).unwrap());
  346. m
  347. }),
  348. ];
  349. for (i, n, a) in inputs {
  350. let mut res = ContentParser::parse(Rule::inline_shortcode, i).unwrap();
  351. let (name, args) = parse_shortcode_call(res.next().unwrap());
  352. assert_eq!(name, n);
  353. assert_eq!(args, a);
  354. }
  355. }
  356. #[test]
  357. fn can_render_inline_shortcodes() {
  358. let mut tera = Tera::default();
  359. tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap();
  360. let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera);
  361. assert_eq!(res, "Inline Hello 1.");
  362. }
  363. #[test]
  364. fn can_render_shortcodes_with_body() {
  365. let mut tera = Tera::default();
  366. tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap();
  367. let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera);
  368. assert_eq!(res, "Body\n Hey!");
  369. }
  370. // https://github.com/Keats/gutenberg/issues/462
  371. #[test]
  372. fn shortcodes_with_body_do_not_eat_newlines() {
  373. let mut tera = Tera::default();
  374. tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap();
  375. let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera);
  376. assert_eq!(res, "Body\n Hello \n World");
  377. }
  378. #[test]
  379. fn outer_newlines_removed_from_shortcodes_with_body() {
  380. let mut tera = Tera::default();
  381. tera.add_raw_template("shortcodes/youtube.html", " \n {{body}} \n ").unwrap();
  382. let res = render_shortcodes("\n{% youtube() %} \n content \n {% end %}\n", &tera);
  383. assert_eq!(res, "\n content \n");
  384. }
  385. #[test]
  386. fn outer_newlines_removed_from_inline_shortcodes() {
  387. let mut tera = Tera::default();
  388. tera.add_raw_template("shortcodes/youtube.html", " \n Hello, Zola. \n ").unwrap();
  389. let res = render_shortcodes("\n{{ youtube() }}\n", &tera);
  390. assert_eq!(res, "\n Hello, Zola. \n");
  391. }
  392. }