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.

406 lines
15KB

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