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.

337 lines
11KB

  1. use pest::Parser;
  2. use pest::iterators::Pair;
  3. use tera::{Tera, Map, Context, Value, to_value};
  4. use errors::{Result, ResultExt};
  5. use config::Config;
  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: String, args: Map<String, Value>, tera: &Tera, config: &Config, body: Option<&str>) -> Result<String> {
  76. let mut context = Context::new();
  77. for (key, value) in args.iter() {
  78. context.insert(key, value);
  79. }
  80. if let Some(ref b) = body {
  81. context.insert("body", b);
  82. }
  83. context.insert("config", config);
  84. let tpl_name = format!("shortcodes/{}.html", name);
  85. tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
  86. }
  87. pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result<String> {
  88. // Don't do anything if there is nothing like a shortcode in the content
  89. if !content.contains("{{") && !content.contains("{%") {
  90. return Ok(content.to_string());
  91. }
  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(_) => panic!("TODO"), // TODO: error handling
  96. };
  97. // We have at least a `page` pair
  98. for p in pairs.next().unwrap().into_inner() {
  99. match p.as_rule() {
  100. Rule::text | Rule::text_in_ignored_body_sc | Rule::text_in_body_sc => res.push_str(p.into_span().as_str()),
  101. Rule::inline_shortcode => {
  102. let (name, args) = parse_shortcode_call(p);
  103. res.push_str(&render_shortcode(name, args, tera, config, None)?);
  104. },
  105. Rule::shortcode_with_body => {
  106. let mut inner = p.into_inner();
  107. // 3 items in inner: call, body, end
  108. // we don't care about the closing tag
  109. let (name, args) = parse_shortcode_call(inner.next().unwrap());
  110. let body = inner.next().unwrap().into_span().as_str();
  111. res.push_str(&render_shortcode(name, args, tera, config, Some(body))?);
  112. },
  113. Rule::ignored_inline_shortcode => {
  114. res.push_str(
  115. &p.into_span().as_str()
  116. .replacen("{{/*", "{{", 1)
  117. .replacen("*/}}", "}}", 1)
  118. );
  119. },
  120. Rule::ignored_shortcode_with_body => {
  121. for p2 in p.into_inner() {
  122. match p2.as_rule() {
  123. Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => {
  124. res.push_str(
  125. &p2.into_span().as_str()
  126. .replacen("{%/*", "{%", 1)
  127. .replacen("*/%}", "%}", 1)
  128. );
  129. },
  130. Rule::text_in_ignored_body_sc => res.push_str(p2.into_span().as_str()),
  131. _ => unreachable!("Got something weird in an ignored shortcode"),
  132. }
  133. }
  134. },
  135. _ => unreachable!("unexpected page rule: {:?}", p.as_rule()),
  136. }
  137. }
  138. Ok(res)
  139. }
  140. #[cfg(test)]
  141. mod tests {
  142. use super::*;
  143. macro_rules! assert_lex_rule {
  144. ($rule: expr, $input: expr) => {
  145. let res = ContentParser::parse($rule, $input);
  146. println!("{:?}", $input);
  147. println!("{:#?}", res);
  148. if res.is_err() {
  149. println!("{}", res.unwrap_err());
  150. panic!();
  151. }
  152. assert!(res.is_ok());
  153. assert_eq!(res.unwrap().last().unwrap().into_span().end(), $input.len());
  154. };
  155. }
  156. #[test]
  157. fn lex_text() {
  158. let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"];
  159. for i in inputs {
  160. assert_lex_rule!(Rule::text, i);
  161. }
  162. }
  163. #[test]
  164. fn lex_inline_shortcode() {
  165. let inputs = vec![
  166. "{{ youtube() }}",
  167. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  168. "{{ youtube(id=1, \nautoplay=true, url='hey') }}",
  169. ];
  170. for i in inputs {
  171. assert_lex_rule!(Rule::inline_shortcode, i);
  172. }
  173. }
  174. #[test]
  175. fn lex_inline_ignored_shortcode() {
  176. let inputs = vec![
  177. "{{/* youtube() */}}",
  178. "{{/* youtube(id=1, autoplay=true, url='hey') */}}",
  179. "{{/* youtube(id=1, \nautoplay=true, \nurl='hey') */}}",
  180. ];
  181. for i in inputs {
  182. assert_lex_rule!(Rule::ignored_inline_shortcode, i);
  183. }
  184. }
  185. #[test]
  186. fn lex_shortcode_with_body() {
  187. let inputs = vec![
  188. r#"{% youtube() %}
  189. Some text
  190. {% end %}"#,
  191. r#"{% youtube(id=1,
  192. autoplay=true, url='hey') %}
  193. Some text
  194. {% end %}"#,
  195. ];
  196. for i in inputs {
  197. assert_lex_rule!(Rule::shortcode_with_body, i);
  198. }
  199. }
  200. #[test]
  201. fn lex_ignored_shortcode_with_body() {
  202. let inputs = vec![
  203. r#"{%/* youtube() */%}
  204. Some text
  205. {%/* end */%}"#,
  206. r#"{%/* youtube(id=1,
  207. autoplay=true, url='hey') */%}
  208. Some text
  209. {%/* end */%}"#,
  210. ];
  211. for i in inputs {
  212. assert_lex_rule!(Rule::ignored_shortcode_with_body, i);
  213. }
  214. }
  215. #[test]
  216. fn lex_page() {
  217. let inputs = vec![
  218. "Some text and a shortcode `{{/* youtube() */}}`",
  219. "{{ youtube(id=1, autoplay=true, url='hey') }}",
  220. "{{ youtube(id=1, \nautoplay=true, url='hey') }} that's it",
  221. r#"
  222. This is a test
  223. {% hello() %}
  224. Body {{ var }}
  225. {% end %}
  226. "#
  227. ];
  228. for i in inputs {
  229. assert_lex_rule!(Rule::page, i);
  230. }
  231. }
  232. #[test]
  233. fn does_nothing_with_no_shortcodes() {
  234. let res = render_shortcodes("Hello World", &Tera::default(), &Config::default());
  235. assert_eq!(res.unwrap(), "Hello World");
  236. }
  237. #[test]
  238. fn can_unignore_inline_shortcode() {
  239. let res = render_shortcodes(
  240. "Hello World {{/* youtube() */}}",
  241. &Tera::default(),
  242. &Config::default(),
  243. );
  244. assert_eq!(res.unwrap(), "Hello World {{ youtube() }}");
  245. }
  246. #[test]
  247. fn can_unignore_shortcode_with_body() {
  248. let res = render_shortcodes(r#"
  249. Hello World
  250. {%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default(), &Config::default());
  251. assert_eq!(res.unwrap(), "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}");
  252. }
  253. #[test]
  254. fn can_parse_shortcode_arguments() {
  255. let inputs = vec![
  256. ("{{ youtube() }}", "youtube", Map::new()),
  257. (
  258. "{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}",
  259. "youtube",
  260. {
  261. let mut m = Map::new();
  262. m.insert("id".to_string(), to_value(1).unwrap());
  263. m.insert("autoplay".to_string(), to_value(true).unwrap());
  264. m.insert("hello".to_string(), to_value("salut").unwrap());
  265. m.insert("float".to_string(), to_value(1.2).unwrap());
  266. m
  267. }
  268. ),
  269. (
  270. "{{ gallery(photos=['something', 'else'], fullscreen=true) }}",
  271. "gallery",
  272. {
  273. let mut m = Map::new();
  274. m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
  275. m.insert("fullscreen".to_string(), to_value(true).unwrap());
  276. m
  277. }
  278. ),
  279. ];
  280. for (i, n, a) in inputs {
  281. let mut res = ContentParser::parse(Rule::inline_shortcode, i).unwrap();
  282. let (name, args) = parse_shortcode_call(res.next().unwrap());
  283. assert_eq!(name, n);
  284. assert_eq!(args, a);
  285. }
  286. }
  287. #[test]
  288. fn can_render_inline_shortcodes() {
  289. let mut tera = Tera::default();
  290. tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap();
  291. let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera, &Config::default()).unwrap();
  292. assert_eq!(res, "Inline Hello 1.");
  293. }
  294. #[test]
  295. fn can_render_shortcodes_with_body() {
  296. let mut tera = Tera::default();
  297. tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap();
  298. let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap();
  299. assert_eq!(res, "Body\n Hey!");
  300. }
  301. }