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.

415 lines
15KB

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