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.

398 lines
15KB

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