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.

markdown.rs 28KB

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
Allow manual specification of header IDs (#685) Justification for this feature is added in the docs. Precedent for the precise syntax: Hugo. Hugo puts this syntax behind a preference named headerIds, and automatic header ID generation behind a preference named autoHeaderIds, with both enabled by default. I have not implemented a switch to disable this. My suggestion for a workaround for the improbable case of desiring a literal “{#…}” at the end of a header is to replace `}` with `&#125;`. The algorithm I have used is not identical to [that which Hugo uses][0], because Hugo’s looks to work at the source level, whereas here we work at the pulldown-cmark event level, which is generally more sane, but potentially limiting for extremely esoteric IDs. Practical differences in implementation from Hugo (based purely on reading [blackfriday’s implementation][0], not actually trying it): - I believe Hugo would treat `# Foo {#*bar*}` as a heading with text “Foo” and ID `*bar*`, since it is working at the source level; whereas this code turns it into a heading with HTML `Foo {#<em>bar</em>}`, as it works at the pulldown-cmark event level and doesn’t go out of its way to make that work (I’m not familiar with pulldown-cmark, but I get the impression that you could make it work Hugo’s way on this point). The difference should be negligible: only *very* esoteric hashes would include magic Markdown characters. - Hugo will automatically generate an ID for `{#}`, whereas what I’ve coded here will yield a blank ID instead (which feels more correct to me—`None` versus `Some("")`, and all that). In practice the results should be identical. Fixes #433. [0]: https://github.com/russross/blackfriday/blob/a477dd1646916742841ed20379f941cfa6c5bb6f/block.go#L218-L234
5 years ago
Allow manual specification of header IDs (#685) Justification for this feature is added in the docs. Precedent for the precise syntax: Hugo. Hugo puts this syntax behind a preference named headerIds, and automatic header ID generation behind a preference named autoHeaderIds, with both enabled by default. I have not implemented a switch to disable this. My suggestion for a workaround for the improbable case of desiring a literal “{#…}” at the end of a header is to replace `}` with `&#125;`. The algorithm I have used is not identical to [that which Hugo uses][0], because Hugo’s looks to work at the source level, whereas here we work at the pulldown-cmark event level, which is generally more sane, but potentially limiting for extremely esoteric IDs. Practical differences in implementation from Hugo (based purely on reading [blackfriday’s implementation][0], not actually trying it): - I believe Hugo would treat `# Foo {#*bar*}` as a heading with text “Foo” and ID `*bar*`, since it is working at the source level; whereas this code turns it into a heading with HTML `Foo {#<em>bar</em>}`, as it works at the pulldown-cmark event level and doesn’t go out of its way to make that work (I’m not familiar with pulldown-cmark, but I get the impression that you could make it work Hugo’s way on this point). The difference should be negligible: only *very* esoteric hashes would include magic Markdown characters. - Hugo will automatically generate an ID for `{#}`, whereas what I’ve coded here will yield a blank ID instead (which feels more correct to me—`None` versus `Some("")`, and all that). In practice the results should be identical. Fixes #433. [0]: https://github.com/russross/blackfriday/blob/a477dd1646916742841ed20379f941cfa6c5bb6f/block.go#L218-L234
5 years ago
Allow manual specification of header IDs (#685) Justification for this feature is added in the docs. Precedent for the precise syntax: Hugo. Hugo puts this syntax behind a preference named headerIds, and automatic header ID generation behind a preference named autoHeaderIds, with both enabled by default. I have not implemented a switch to disable this. My suggestion for a workaround for the improbable case of desiring a literal “{#…}” at the end of a header is to replace `}` with `&#125;`. The algorithm I have used is not identical to [that which Hugo uses][0], because Hugo’s looks to work at the source level, whereas here we work at the pulldown-cmark event level, which is generally more sane, but potentially limiting for extremely esoteric IDs. Practical differences in implementation from Hugo (based purely on reading [blackfriday’s implementation][0], not actually trying it): - I believe Hugo would treat `# Foo {#*bar*}` as a heading with text “Foo” and ID `*bar*`, since it is working at the source level; whereas this code turns it into a heading with HTML `Foo {#<em>bar</em>}`, as it works at the pulldown-cmark event level and doesn’t go out of its way to make that work (I’m not familiar with pulldown-cmark, but I get the impression that you could make it work Hugo’s way on this point). The difference should be negligible: only *very* esoteric hashes would include magic Markdown characters. - Hugo will automatically generate an ID for `{#}`, whereas what I’ve coded here will yield a blank ID instead (which feels more correct to me—`None` versus `Some("")`, and all that). In practice the results should be identical. Fixes #433. [0]: https://github.com/russross/blackfriday/blob/a477dd1646916742841ed20379f941cfa6c5bb6f/block.go#L218-L234
5 years ago
Allow manual specification of header IDs (#685) Justification for this feature is added in the docs. Precedent for the precise syntax: Hugo. Hugo puts this syntax behind a preference named headerIds, and automatic header ID generation behind a preference named autoHeaderIds, with both enabled by default. I have not implemented a switch to disable this. My suggestion for a workaround for the improbable case of desiring a literal “{#…}” at the end of a header is to replace `}` with `&#125;`. The algorithm I have used is not identical to [that which Hugo uses][0], because Hugo’s looks to work at the source level, whereas here we work at the pulldown-cmark event level, which is generally more sane, but potentially limiting for extremely esoteric IDs. Practical differences in implementation from Hugo (based purely on reading [blackfriday’s implementation][0], not actually trying it): - I believe Hugo would treat `# Foo {#*bar*}` as a heading with text “Foo” and ID `*bar*`, since it is working at the source level; whereas this code turns it into a heading with HTML `Foo {#<em>bar</em>}`, as it works at the pulldown-cmark event level and doesn’t go out of its way to make that work (I’m not familiar with pulldown-cmark, but I get the impression that you could make it work Hugo’s way on this point). The difference should be negligible: only *very* esoteric hashes would include magic Markdown characters. - Hugo will automatically generate an ID for `{#}`, whereas what I’ve coded here will yield a blank ID instead (which feels more correct to me—`None` versus `Some("")`, and all that). In practice the results should be identical. Fixes #433. [0]: https://github.com/russross/blackfriday/blob/a477dd1646916742841ed20379f941cfa6c5bb6f/block.go#L218-L234
5 years ago
Allow manual specification of header IDs (#685) Justification for this feature is added in the docs. Precedent for the precise syntax: Hugo. Hugo puts this syntax behind a preference named headerIds, and automatic header ID generation behind a preference named autoHeaderIds, with both enabled by default. I have not implemented a switch to disable this. My suggestion for a workaround for the improbable case of desiring a literal “{#…}” at the end of a header is to replace `}` with `&#125;`. The algorithm I have used is not identical to [that which Hugo uses][0], because Hugo’s looks to work at the source level, whereas here we work at the pulldown-cmark event level, which is generally more sane, but potentially limiting for extremely esoteric IDs. Practical differences in implementation from Hugo (based purely on reading [blackfriday’s implementation][0], not actually trying it): - I believe Hugo would treat `# Foo {#*bar*}` as a heading with text “Foo” and ID `*bar*`, since it is working at the source level; whereas this code turns it into a heading with HTML `Foo {#<em>bar</em>}`, as it works at the pulldown-cmark event level and doesn’t go out of its way to make that work (I’m not familiar with pulldown-cmark, but I get the impression that you could make it work Hugo’s way on this point). The difference should be negligible: only *very* esoteric hashes would include magic Markdown characters. - Hugo will automatically generate an ID for `{#}`, whereas what I’ve coded here will yield a blank ID instead (which feels more correct to me—`None` versus `Some("")`, and all that). In practice the results should be identical. Fixes #433. [0]: https://github.com/russross/blackfriday/blob/a477dd1646916742841ed20379f941cfa6c5bb6f/block.go#L218-L234
5 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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. use std::collections::HashMap;
  2. use tera::Tera;
  3. use config::Config;
  4. use front_matter::InsertAnchor;
  5. use rendering::{render_content, RenderContext};
  6. use templates::ZOLA_TERA;
  7. #[test]
  8. fn can_do_render_content_simple() {
  9. let tera_ctx = Tera::default();
  10. let permalinks_ctx = HashMap::new();
  11. let config = Config::default();
  12. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  13. let res = render_content("hello", &context).unwrap();
  14. assert_eq!(res.body, "<p>hello</p>\n");
  15. }
  16. #[test]
  17. fn doesnt_highlight_code_block_with_highlighting_off() {
  18. let tera_ctx = Tera::default();
  19. let permalinks_ctx = HashMap::new();
  20. let mut config = Config::default();
  21. config.highlight_code = false;
  22. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  23. let res = render_content("```\n$ gutenberg server\n```", &context).unwrap();
  24. assert_eq!(res.body, "<pre><code>$ gutenberg server\n</code></pre>\n");
  25. }
  26. #[test]
  27. fn can_highlight_code_block_no_lang() {
  28. let tera_ctx = Tera::default();
  29. let permalinks_ctx = HashMap::new();
  30. let mut config = Config::default();
  31. config.highlight_code = true;
  32. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  33. let res = render_content("```\n$ gutenberg server\n$ ping\n```", &context).unwrap();
  34. assert_eq!(
  35. res.body,
  36. "<pre style=\"background-color:#2b303b;\">\n<span style=\"color:#c0c5ce;\">$ gutenberg server\n$ ping\n</span></pre>"
  37. );
  38. }
  39. #[test]
  40. fn can_highlight_code_block_with_lang() {
  41. let tera_ctx = Tera::default();
  42. let permalinks_ctx = HashMap::new();
  43. let mut config = Config::default();
  44. config.highlight_code = true;
  45. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  46. let res = render_content("```python\nlist.append(1)\n```", &context).unwrap();
  47. assert_eq!(
  48. res.body,
  49. "<pre style=\"background-color:#2b303b;\">\n<span style=\"color:#c0c5ce;\">list.</span><span style=\"color:#bf616a;\">append</span><span style=\"color:#c0c5ce;\">(</span><span style=\"color:#d08770;\">1</span><span style=\"color:#c0c5ce;\">)\n</span></pre>"
  50. );
  51. }
  52. #[test]
  53. fn can_higlight_code_block_with_unknown_lang() {
  54. let tera_ctx = Tera::default();
  55. let permalinks_ctx = HashMap::new();
  56. let mut config = Config::default();
  57. config.highlight_code = true;
  58. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  59. let res = render_content("```yolo\nlist.append(1)\n```", &context).unwrap();
  60. // defaults to plain text
  61. assert_eq!(
  62. res.body,
  63. "<pre style=\"background-color:#2b303b;\">\n<span style=\"color:#c0c5ce;\">list.append(1)\n</span></pre>"
  64. );
  65. }
  66. #[test]
  67. fn can_render_shortcode() {
  68. let permalinks_ctx = HashMap::new();
  69. let config = Config::default();
  70. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  71. let res = render_content(
  72. r#"
  73. Hello
  74. {{ youtube(id="ub36ffWAqgQ") }}
  75. "#,
  76. &context,
  77. )
  78. .unwrap();
  79. assert!(res.body.contains("<p>Hello</p>\n<div >"));
  80. assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
  81. }
  82. #[test]
  83. fn can_render_shortcode_with_markdown_char_in_args_name() {
  84. let permalinks_ctx = HashMap::new();
  85. let config = Config::default();
  86. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  87. let input = vec!["name", "na_me", "n_a_me", "n1"];
  88. for i in input {
  89. let res =
  90. render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap();
  91. assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#));
  92. }
  93. }
  94. #[test]
  95. fn can_render_shortcode_with_markdown_char_in_args_value() {
  96. let permalinks_ctx = HashMap::new();
  97. let config = Config::default();
  98. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  99. let input = vec![
  100. "ub36ffWAqgQ-hey",
  101. "ub36ffWAqgQ_hey",
  102. "ub36ffWAqgQ_he_y",
  103. "ub36ffWAqgQ*hey",
  104. "ub36ffWAqgQ#hey",
  105. ];
  106. for i in input {
  107. let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap();
  108. assert!(res
  109. .body
  110. .contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i)));
  111. }
  112. }
  113. #[test]
  114. fn can_render_body_shortcode_with_markdown_char_in_name() {
  115. let permalinks_ctx = HashMap::new();
  116. let mut tera = Tera::default();
  117. tera.extend(&ZOLA_TERA).unwrap();
  118. let input = vec!["quo_te", "qu_o_te"];
  119. let config = Config::default();
  120. for i in input {
  121. tera.add_raw_template(
  122. &format!("shortcodes/{}.html", i),
  123. "<blockquote>{{ body }} - {{ author}}</blockquote>",
  124. )
  125. .unwrap();
  126. let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  127. let res =
  128. render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context)
  129. .unwrap();
  130. println!("{:?}", res);
  131. assert!(res.body.contains("<blockquote>hey - Bob</blockquote>"));
  132. }
  133. }
  134. #[test]
  135. fn can_render_body_shortcode_and_paragraph_after() {
  136. let permalinks_ctx = HashMap::new();
  137. let mut tera = Tera::default();
  138. tera.extend(&ZOLA_TERA).unwrap();
  139. let shortcode = "<p>{{ body }}</p>";
  140. let markdown_string = r#"
  141. {% figure() %}
  142. This is a figure caption.
  143. {% end %}
  144. Here is another paragraph.
  145. "#;
  146. let expected = "<p>This is a figure caption.</p>
  147. <p>Here is another paragraph.</p>
  148. ";
  149. tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
  150. let config = Config::default();
  151. let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  152. let res = render_content(markdown_string, &context).unwrap();
  153. println!("{:?}", res);
  154. assert_eq!(res.body, expected);
  155. }
  156. #[test]
  157. fn can_render_two_body_shortcode_and_paragraph_after_with_line_break_between() {
  158. let permalinks_ctx = HashMap::new();
  159. let mut tera = Tera::default();
  160. tera.extend(&ZOLA_TERA).unwrap();
  161. let shortcode = "<p>{{ body }}</p>";
  162. let markdown_string = r#"
  163. {% figure() %}
  164. This is a figure caption.
  165. {% end %}
  166. {% figure() %}
  167. This is a figure caption.
  168. {% end %}
  169. Here is another paragraph.
  170. "#;
  171. let expected = "<p>This is a figure caption.</p>
  172. <p>This is a figure caption.</p>
  173. <p>Here is another paragraph.</p>
  174. ";
  175. tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
  176. let config = Config::default();
  177. let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  178. let res = render_content(markdown_string, &context).unwrap();
  179. println!("{:?}", res);
  180. assert_eq!(res.body, expected);
  181. }
  182. #[test]
  183. fn can_render_several_shortcode_in_row() {
  184. let permalinks_ctx = HashMap::new();
  185. let config = Config::default();
  186. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  187. let res = render_content(
  188. r#"
  189. Hello
  190. {{ youtube(id="ub36ffWAqgQ") }}
  191. {{ youtube(id="ub36ffWAqgQ", autoplay=true) }}
  192. {{ vimeo(id="210073083") }}
  193. {{ streamable(id="c0ic") }}
  194. {{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}
  195. "#,
  196. &context,
  197. )
  198. .unwrap();
  199. assert!(res.body.contains("<p>Hello</p>\n<div >"));
  200. assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
  201. assert!(res
  202. .body
  203. .contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#));
  204. assert!(res.body.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#));
  205. assert!(res.body.contains(r#"//player.vimeo.com/video/210073083""#));
  206. }
  207. #[test]
  208. fn doesnt_render_ignored_shortcodes() {
  209. let permalinks_ctx = HashMap::new();
  210. let mut config = Config::default();
  211. config.highlight_code = false;
  212. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  213. let res = render_content(r#"```{{/* youtube(id="w7Ft2ymGmfc") */}}```"#, &context).unwrap();
  214. assert_eq!(res.body, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n");
  215. }
  216. #[test]
  217. fn can_render_shortcode_with_body() {
  218. let mut tera = Tera::default();
  219. tera.extend(&ZOLA_TERA).unwrap();
  220. tera.add_raw_template(
  221. "shortcodes/quote.html",
  222. "<blockquote>{{ body }} - {{ author }}</blockquote>",
  223. )
  224. .unwrap();
  225. let permalinks_ctx = HashMap::new();
  226. let config = Config::default();
  227. let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  228. let res = render_content(
  229. r#"
  230. Hello
  231. {% quote(author="Keats") %}
  232. A quote
  233. {% end %}
  234. "#,
  235. &context,
  236. )
  237. .unwrap();
  238. assert_eq!(res.body, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n");
  239. }
  240. #[test]
  241. fn errors_rendering_unknown_shortcode() {
  242. let tera_ctx = Tera::default();
  243. let permalinks_ctx = HashMap::new();
  244. let config = Config::default();
  245. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  246. let res = render_content("{{ hello(flash=true) }}", &context);
  247. assert!(res.is_err());
  248. }
  249. #[test]
  250. fn can_make_valid_relative_link() {
  251. let mut permalinks = HashMap::new();
  252. permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
  253. let tera_ctx = Tera::default();
  254. let config = Config::default();
  255. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
  256. let res = render_content(
  257. r#"[rel link](@/pages/about.md), [abs link](https://vincent.is/about)"#,
  258. &context,
  259. )
  260. .unwrap();
  261. assert!(
  262. res.body.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#)
  263. );
  264. }
  265. #[test]
  266. fn can_make_relative_links_with_anchors() {
  267. let mut permalinks = HashMap::new();
  268. permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
  269. let tera_ctx = Tera::default();
  270. let config = Config::default();
  271. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
  272. let res = render_content(r#"[rel link](@/pages/about.md#cv)"#, &context).unwrap();
  273. assert!(res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#));
  274. }
  275. #[test]
  276. fn errors_relative_link_inexistant() {
  277. let tera_ctx = Tera::default();
  278. let permalinks_ctx = HashMap::new();
  279. let config = Config::default();
  280. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  281. let res = render_content("[rel link](@/pages/about.md)", &context);
  282. assert!(res.is_err());
  283. }
  284. #[test]
  285. fn can_add_id_to_headings() {
  286. let tera_ctx = Tera::default();
  287. let permalinks_ctx = HashMap::new();
  288. let config = Config::default();
  289. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  290. let res = render_content(r#"# Hello"#, &context).unwrap();
  291. assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n");
  292. }
  293. #[test]
  294. fn can_add_id_to_headings_same_slug() {
  295. let tera_ctx = Tera::default();
  296. let permalinks_ctx = HashMap::new();
  297. let config = Config::default();
  298. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  299. let res = render_content("# Hello\n# Hello", &context).unwrap();
  300. assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
  301. }
  302. #[test]
  303. fn can_add_non_slug_id_to_headings() {
  304. let tera_ctx = Tera::default();
  305. let permalinks_ctx = HashMap::new();
  306. let mut config = Config::default();
  307. config.slugify_paths = false;
  308. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  309. let res = render_content(r#"# L'écologie et vous"#, &context).unwrap();
  310. assert_eq!(res.body, "<h1 id=\"L'écologie_et_vous\">L'écologie et vous</h1>\n");
  311. }
  312. #[test]
  313. fn can_handle_manual_ids_on_headings() {
  314. let tera_ctx = Tera::default();
  315. let permalinks_ctx = HashMap::new();
  316. let config = Config::default();
  317. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  318. // Tested things: manual IDs; whitespace flexibility; that automatic IDs avoid collision with
  319. // manual IDs; that duplicates are in fact permitted among manual IDs; that any non-plain-text
  320. // in the middle of `{#…}` will disrupt it from being acknowledged as a manual ID (that last
  321. // one could reasonably be considered a bug rather than a feature, but test it either way); one
  322. // workaround for the improbable case where you actually want `{#…}` at the end of a heading.
  323. let res = render_content(
  324. "\
  325. # Hello\n\
  326. # Hello{#hello}\n\
  327. # Hello {#hello}\n\
  328. # Hello {#Something_else} \n\
  329. # Workaround for literal {#…&#125;\n\
  330. # Hello\n\
  331. # Auto {#*matic*}",
  332. &context,
  333. )
  334. .unwrap();
  335. assert_eq!(
  336. res.body,
  337. "\
  338. <h1 id=\"hello-1\">Hello</h1>\n\
  339. <h1 id=\"hello\">Hello</h1>\n\
  340. <h1 id=\"hello\">Hello</h1>\n\
  341. <h1 id=\"Something_else\">Hello</h1>\n\
  342. <h1 id=\"workaround-for-literal\">Workaround for literal {#…}</h1>\n\
  343. <h1 id=\"hello-2\">Hello</h1>\n\
  344. <h1 id=\"auto-matic\">Auto {#<em>matic</em>}</h1>\n\
  345. "
  346. );
  347. }
  348. #[test]
  349. fn blank_headings() {
  350. let tera_ctx = Tera::default();
  351. let permalinks_ctx = HashMap::new();
  352. let config = Config::default();
  353. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  354. let res = render_content("# \n#\n# {#hmm} \n# {#}", &context).unwrap();
  355. assert_eq!(
  356. res.body,
  357. "<h1 id=\"-1\"></h1>\n<h1 id=\"-2\"></h1>\n<h1 id=\"hmm\"></h1>\n<h1 id=\"\"></h1>\n"
  358. );
  359. }
  360. #[test]
  361. fn can_insert_anchor_left() {
  362. let permalinks_ctx = HashMap::new();
  363. let config = Config::default();
  364. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
  365. let res = render_content("# Hello", &context).unwrap();
  366. assert_eq!(
  367. res.body,
  368. "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello</h1>\n"
  369. );
  370. }
  371. #[test]
  372. fn can_insert_anchor_right() {
  373. let permalinks_ctx = HashMap::new();
  374. let config = Config::default();
  375. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right);
  376. let res = render_content("# Hello", &context).unwrap();
  377. assert_eq!(
  378. res.body,
  379. "<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a></h1>\n"
  380. );
  381. }
  382. #[test]
  383. fn can_insert_anchor_for_multi_heading() {
  384. let permalinks_ctx = HashMap::new();
  385. let config = Config::default();
  386. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right);
  387. let res = render_content("# Hello\n# World", &context).unwrap();
  388. assert_eq!(
  389. res.body,
  390. "<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a></h1>\n\
  391. <h1 id=\"world\">World<a class=\"zola-anchor\" href=\"#world\" aria-label=\"Anchor link for: world\">🔗</a></h1>\n"
  392. );
  393. }
  394. // See https://github.com/Keats/gutenberg/issues/42
  395. #[test]
  396. fn can_insert_anchor_with_exclamation_mark() {
  397. let permalinks_ctx = HashMap::new();
  398. let config = Config::default();
  399. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
  400. let res = render_content("# Hello!", &context).unwrap();
  401. assert_eq!(
  402. res.body,
  403. "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello!</h1>\n"
  404. );
  405. }
  406. // See https://github.com/Keats/gutenberg/issues/53
  407. #[test]
  408. fn can_insert_anchor_with_link() {
  409. let permalinks_ctx = HashMap::new();
  410. let config = Config::default();
  411. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
  412. let res = render_content("## [Rust](https://rust-lang.org)", &context).unwrap();
  413. assert_eq!(
  414. res.body,
  415. "<h2 id=\"rust\"><a class=\"zola-anchor\" href=\"#rust\" aria-label=\"Anchor link for: rust\">🔗</a><a href=\"https://rust-lang.org\">Rust</a></h2>\n"
  416. );
  417. }
  418. #[test]
  419. fn can_insert_anchor_with_other_special_chars() {
  420. let permalinks_ctx = HashMap::new();
  421. let config = Config::default();
  422. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
  423. let res = render_content("# Hello*_()", &context).unwrap();
  424. assert_eq!(
  425. res.body,
  426. "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello*_()</h1>\n"
  427. );
  428. }
  429. #[test]
  430. fn can_make_toc() {
  431. let permalinks_ctx = HashMap::new();
  432. let config = Config::default();
  433. let context = RenderContext::new(
  434. &ZOLA_TERA,
  435. &config,
  436. "https://mysite.com/something",
  437. &permalinks_ctx,
  438. InsertAnchor::Left,
  439. );
  440. let res = render_content(
  441. r#"
  442. # Heading 1
  443. ## Heading 2
  444. ## Another Heading 2
  445. ### Last one
  446. "#,
  447. &context,
  448. )
  449. .unwrap();
  450. let toc = res.toc;
  451. assert_eq!(toc.len(), 1);
  452. assert_eq!(toc[0].children.len(), 2);
  453. assert_eq!(toc[0].children[1].children.len(), 1);
  454. }
  455. #[test]
  456. fn can_ignore_tags_in_toc() {
  457. let permalinks_ctx = HashMap::new();
  458. let config = Config::default();
  459. let context = RenderContext::new(
  460. &ZOLA_TERA,
  461. &config,
  462. "https://mysite.com/something",
  463. &permalinks_ctx,
  464. InsertAnchor::Left,
  465. );
  466. let res = render_content(
  467. r#"
  468. ## heading with `code`
  469. ## [anchor](https://duckduckgo.com/) in heading
  470. ## **bold** and *italics*
  471. "#,
  472. &context,
  473. )
  474. .unwrap();
  475. let toc = res.toc;
  476. assert_eq!(toc[0].id, "heading-with-code");
  477. assert_eq!(toc[0].title, "heading with code");
  478. assert_eq!(toc[1].id, "anchor-in-heading");
  479. assert_eq!(toc[1].title, "anchor in heading");
  480. assert_eq!(toc[2].id, "bold-and-italics");
  481. assert_eq!(toc[2].title, "bold and italics");
  482. }
  483. #[test]
  484. fn can_understand_backtick_in_titles() {
  485. let permalinks_ctx = HashMap::new();
  486. let config = Config::default();
  487. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  488. let res = render_content("# `Hello`", &context).unwrap();
  489. assert_eq!(res.body, "<h1 id=\"hello\"><code>Hello</code></h1>\n");
  490. }
  491. #[test]
  492. fn can_understand_backtick_in_paragraphs() {
  493. let permalinks_ctx = HashMap::new();
  494. let config = Config::default();
  495. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  496. let res = render_content("Hello `world`", &context).unwrap();
  497. assert_eq!(res.body, "<p>Hello <code>world</code></p>\n");
  498. }
  499. // https://github.com/Keats/gutenberg/issues/297
  500. #[test]
  501. fn can_understand_links_in_heading() {
  502. let permalinks_ctx = HashMap::new();
  503. let config = Config::default();
  504. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  505. let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap();
  506. assert_eq!(res.body, "<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n");
  507. }
  508. #[test]
  509. fn can_understand_link_with_title_in_heading() {
  510. let permalinks_ctx = HashMap::new();
  511. let config = Config::default();
  512. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  513. let res =
  514. render_content("# [Rust](https://rust-lang.org \"Rust homepage\")", &context).unwrap();
  515. assert_eq!(
  516. res.body,
  517. "<h1 id=\"rust\"><a href=\"https://rust-lang.org\" title=\"Rust homepage\">Rust</a></h1>\n"
  518. );
  519. }
  520. #[test]
  521. fn can_understand_emphasis_in_heading() {
  522. let permalinks_ctx = HashMap::new();
  523. let config = Config::default();
  524. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  525. let res = render_content("# *Emphasis* text", &context).unwrap();
  526. assert_eq!(res.body, "<h1 id=\"emphasis-text\"><em>Emphasis</em> text</h1>\n");
  527. }
  528. #[test]
  529. fn can_understand_strong_in_heading() {
  530. let permalinks_ctx = HashMap::new();
  531. let config = Config::default();
  532. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  533. let res = render_content("# **Strong** text", &context).unwrap();
  534. assert_eq!(res.body, "<h1 id=\"strong-text\"><strong>Strong</strong> text</h1>\n");
  535. }
  536. #[test]
  537. fn can_understand_code_in_heading() {
  538. let permalinks_ctx = HashMap::new();
  539. let config = Config::default();
  540. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  541. let res = render_content("# `Code` text", &context).unwrap();
  542. assert_eq!(res.body, "<h1 id=\"code-text\"><code>Code</code> text</h1>\n");
  543. }
  544. // See https://github.com/getzola/zola/issues/569
  545. #[test]
  546. fn can_understand_footnote_in_heading() {
  547. let permalinks_ctx = HashMap::new();
  548. let config = Config::default();
  549. let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
  550. let res = render_content("# text [^1] there\n[^1]: footnote", &context).unwrap();
  551. assert_eq!(res.body, r##"<h1 id="text-there">text <sup class="footnote-reference"><a href="#1">1</a></sup> there</h1>
  552. <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
  553. <p>footnote</p>
  554. </div>
  555. "##);
  556. }
  557. #[test]
  558. fn can_make_valid_relative_link_in_heading() {
  559. let mut permalinks = HashMap::new();
  560. permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about/".to_string());
  561. let tera_ctx = Tera::default();
  562. let config = Config::default();
  563. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
  564. let res = render_content(r#" # [rel link](@/pages/about.md)"#, &context).unwrap();
  565. assert_eq!(
  566. res.body,
  567. "<h1 id=\"rel-link\"><a href=\"https://vincent.is/about/\">rel link</a></h1>\n"
  568. );
  569. }
  570. #[test]
  571. fn can_make_permalinks_with_colocated_assets_for_link() {
  572. let permalinks_ctx = HashMap::new();
  573. let config = Config::default();
  574. let context = RenderContext::new(
  575. &ZOLA_TERA,
  576. &config,
  577. "https://vincent.is/about/",
  578. &permalinks_ctx,
  579. InsertAnchor::None,
  580. );
  581. let res = render_content("[an image](image.jpg)", &context).unwrap();
  582. assert_eq!(res.body, "<p><a href=\"https://vincent.is/about/image.jpg\">an image</a></p>\n");
  583. }
  584. #[test]
  585. fn can_make_permalinks_with_colocated_assets_for_image() {
  586. let permalinks_ctx = HashMap::new();
  587. let config = Config::default();
  588. let context = RenderContext::new(
  589. &ZOLA_TERA,
  590. &config,
  591. "https://vincent.is/about/",
  592. &permalinks_ctx,
  593. InsertAnchor::None,
  594. );
  595. let res = render_content("![alt text](image.jpg)", &context).unwrap();
  596. assert_eq!(
  597. res.body,
  598. "<p><img src=\"https://vincent.is/about/image.jpg\" alt=\"alt text\" /></p>\n"
  599. );
  600. }
  601. #[test]
  602. fn markdown_doesnt_wrap_html_in_paragraph() {
  603. let permalinks_ctx = HashMap::new();
  604. let config = Config::default();
  605. let context = RenderContext::new(
  606. &ZOLA_TERA,
  607. &config,
  608. "https://vincent.is/about/",
  609. &permalinks_ctx,
  610. InsertAnchor::None,
  611. );
  612. let res = render_content(
  613. r#"
  614. Some text
  615. <h1>Helo</h1>
  616. <div>
  617. <a href="mobx-flow.png">
  618. <img src="mobx-flow.png" alt="MobX flow">
  619. </a>
  620. </div>
  621. "#,
  622. &context,
  623. )
  624. .unwrap();
  625. assert_eq!(
  626. res.body,
  627. "<p>Some text</p>\n<h1>Helo</h1>\n<div>\n<a href=\"mobx-flow.png\">\n <img src=\"mobx-flow.png\" alt=\"MobX flow\">\n </a>\n</div>\n"
  628. );
  629. }
  630. #[test]
  631. fn correctly_captures_external_links() {
  632. let permalinks_ctx = HashMap::new();
  633. let config = Config::default();
  634. let context = RenderContext::new(
  635. &ZOLA_TERA,
  636. &config,
  637. "https://vincent.is/about/",
  638. &permalinks_ctx,
  639. InsertAnchor::None,
  640. );
  641. let content = "
  642. [a link](http://google.com)
  643. [a link](http://google.comy)
  644. Email: [foo@bar.baz](mailto:foo@bar.baz)
  645. Email: <foo@bar.baz>
  646. ";
  647. let res = render_content(content, &context).unwrap();
  648. assert_eq!(
  649. res.external_links,
  650. &["http://google.com".to_owned(), "http://google.comy".to_owned()]
  651. );
  652. }
  653. #[test]
  654. fn can_handle_summaries() {
  655. let tera_ctx = Tera::default();
  656. let permalinks_ctx = HashMap::new();
  657. let config = Config::default();
  658. let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
  659. let res = render_content(
  660. r#"
  661. Hello [My site][world]
  662. <!-- more -->
  663. Bla bla
  664. [world]: https://vincentprouillet.com
  665. "#,
  666. &context,
  667. )
  668. .unwrap();
  669. assert_eq!(
  670. res.body,
  671. "<p>Hello <a href=\"https://vincentprouillet.com\">My site</a></p>\n<p id=\"zola-continue-reading\"><a name=\"continue-reading\"></a></p>\n<p>Bla bla</p>\n"
  672. );
  673. assert_eq!(
  674. res.summary_len,
  675. Some("<p>Hello <a href=\"https://vincentprouillet.com/\">My site</a></p>".len())
  676. );
  677. }
  678. // https://github.com/Keats/gutenberg/issues/522
  679. #[test]
  680. fn doesnt_try_to_highlight_content_from_shortcode() {
  681. let permalinks_ctx = HashMap::new();
  682. let mut tera = Tera::default();
  683. tera.extend(&ZOLA_TERA).unwrap();
  684. let shortcode = r#"
  685. <figure>
  686. {% if width %}
  687. <img src="/images/{{ src }}" alt="{{ caption }}" width="{{ width }}" />
  688. {% else %}
  689. <img src="/images/{{ src }}" alt="{{ caption }}" />
  690. {% endif %}
  691. <figcaption>{{ caption }}</figcaption>
  692. </figure>"#;
  693. let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#;
  694. let expected = r#"<figure>
  695. <img src="/images/spherecluster.png" alt="Some spheres." />
  696. <figcaption>Some spheres.</figcaption>
  697. </figure>"#;
  698. tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
  699. let config = Config::default();
  700. let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  701. let res = render_content(markdown_string, &context).unwrap();
  702. assert_eq!(res.body, expected);
  703. }
  704. // TODO: re-enable once it's fixed in Tera
  705. // https://github.com/Keats/tera/issues/373
  706. //#[test]
  707. //fn can_split_lines_shortcode_body() {
  708. // let permalinks_ctx = HashMap::new();
  709. // let mut tera = Tera::default();
  710. // tera.extend(&ZOLA_TERA).unwrap();
  711. //
  712. // let shortcode = r#"{{ body | split(pat="\n") }}"#;
  713. //
  714. // let markdown_string = r#"
  715. //{% alert() %}
  716. //multi
  717. //ple
  718. //lines
  719. //{% end %}
  720. // "#;
  721. //
  722. // let expected = r#"<p>["multi", "ple", "lines"]</p>"#;
  723. //
  724. // tera.add_raw_template(&format!("shortcodes/{}.html", "alert"), shortcode).unwrap();
  725. // let config = Config::default();
  726. // let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
  727. //
  728. // let res = render_content(markdown_string, &context).unwrap();
  729. // assert_eq!(res.body, expected);
  730. //}
  731. // https://github.com/getzola/zola/issues/747
  732. // https://github.com/getzola/zola/issues/816
  733. #[test]
  734. fn leaves_custom_url_scheme_untouched() {
  735. let content = r#"[foo@bar.tld](xmpp:foo@bar.tld)
  736. [(123) 456-7890](tel:+11234567890)
  737. [blank page](about:blank)
  738. "#;
  739. let tera_ctx = Tera::default();
  740. let config = Config::default();
  741. let permalinks_ctx = HashMap::new();
  742. let context = RenderContext::new(
  743. &tera_ctx,
  744. &config,
  745. "https://vincent.is/",
  746. &permalinks_ctx,
  747. InsertAnchor::None,
  748. );
  749. let res = render_content(content, &context).unwrap();
  750. let expected = r#"<p><a href="xmpp:foo@bar.tld">foo@bar.tld</a></p>
  751. <p><a href="tel:+11234567890">(123) 456-7890</a></p>
  752. <p><a href="about:blank">blank page</a></p>
  753. "#;
  754. assert_eq!(res.body, expected);
  755. }
  756. #[test]
  757. fn stops_with_an_error_on_an_empty_link() {
  758. let content = r#"[some link]()"#;
  759. let tera_ctx = Tera::default();
  760. let config = Config::default();
  761. let permalinks_ctx = HashMap::new();
  762. let context = RenderContext::new(
  763. &tera_ctx,
  764. &config,
  765. "https://vincent.is/",
  766. &permalinks_ctx,
  767. InsertAnchor::None,
  768. );
  769. let res = render_content(content, &context);
  770. let expected = "There is a link that is missing a URL";
  771. assert!(res.is_err());
  772. assert_eq!(res.unwrap_err().to_string(), expected);
  773. }