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