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.

300 lines
12KB

  1. use std::borrow::Cow::Owned;
  2. use pulldown_cmark as cmark;
  3. use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
  4. use slug::slugify;
  5. use syntect::easy::HighlightLines;
  6. use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};
  7. use errors::Result;
  8. use utils::site::resolve_internal_link;
  9. use context::Context;
  10. use highlighting::{SYNTAX_SET, THEME_SET};
  11. use short_code::{SHORTCODE_RE, ShortCode, parse_shortcode, render_simple_shortcode};
  12. use table_of_contents::{TempHeader, Header, make_table_of_contents};
  13. pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec<Header>)> {
  14. // We try to be smart about highlighting code as it can be time-consuming
  15. // If the global config disables it, then we do nothing. However,
  16. // if we see a code block in the content, we assume that this page needs
  17. // to be highlighted. It could potentially have false positive if the content
  18. // has ``` in it but that seems kind of unlikely
  19. let should_highlight = if context.highlight_code {
  20. content.contains("```")
  21. } else {
  22. false
  23. };
  24. // Set while parsing
  25. let mut error = None;
  26. let mut highlighter: Option<HighlightLines> = None;
  27. // the markdown parser will send several Text event if a markdown character
  28. // is present in it, for example `hello_test` will be split in 2: hello and _test.
  29. // Since we can use those chars in shortcode arguments, we need to collect
  30. // the full shortcode somehow first
  31. let mut current_shortcode = String::new();
  32. let mut shortcode_block = None;
  33. // shortcodes live outside of paragraph so we need to ensure we don't close
  34. // a paragraph that has already been closed
  35. let mut added_shortcode = false;
  36. // Don't transform things that look like shortcodes in code blocks
  37. let mut in_code_block = false;
  38. // If we get text in header, we need to insert the id and a anchor
  39. let mut in_header = false;
  40. // pulldown_cmark can send several text events for a title if there are markdown
  41. // specific characters like `!` in them. We only want to insert the anchor the first time
  42. let mut header_created = false;
  43. let mut anchors: Vec<String> = vec![];
  44. // the rendered html
  45. let mut html = String::new();
  46. // We might have cases where the slug is already present in our list of anchor
  47. // for example an article could have several titles named Example
  48. // We add a counter after the slug if the slug is already present, which
  49. // means we will have example, example-1, example-2 etc
  50. fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
  51. if level == 0 && !anchors.contains(&name) {
  52. return name.to_string();
  53. }
  54. let new_anchor = format!("{}-{}", name, level + 1);
  55. if !anchors.contains(&new_anchor) {
  56. return new_anchor;
  57. }
  58. find_anchor(anchors, name, level + 1)
  59. }
  60. let mut headers = vec![];
  61. // Defaults to a 0 level so not a real header
  62. // It should be an Option ideally but not worth the hassle to update
  63. let mut temp_header = TempHeader::default();
  64. let mut clear_shortcode_block = false;
  65. let mut opts = Options::empty();
  66. opts.insert(OPTION_ENABLE_TABLES);
  67. opts.insert(OPTION_ENABLE_FOOTNOTES);
  68. {
  69. let parser = Parser::new_ext(content, opts).map(|event| {
  70. if clear_shortcode_block {
  71. clear_shortcode_block = false;
  72. shortcode_block = None;
  73. }
  74. match event {
  75. Event::Text(mut text) => {
  76. // Header first
  77. if in_header {
  78. if header_created {
  79. temp_header.push(&text);
  80. return Event::Html(Owned(String::new()));
  81. }
  82. let id = find_anchor(&anchors, slugify(&text), 0);
  83. anchors.push(id.clone());
  84. // update the header and add it to the list
  85. temp_header.id = id.clone();
  86. // += as we might have some <code> or other things already there
  87. temp_header.title += &text;
  88. temp_header.permalink = format!("{}#{}", context.current_page_permalink, id);
  89. header_created = true;
  90. return Event::Html(Owned(String::new()));
  91. }
  92. // if we are in the middle of a code block
  93. if let Some(ref mut highlighter) = highlighter {
  94. let highlighted = &highlighter.highlight(&text);
  95. let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
  96. return Event::Html(Owned(html));
  97. }
  98. if in_code_block {
  99. return Event::Text(text);
  100. }
  101. // Are we in the middle of a shortcode that somehow got cut off
  102. // by the markdown parser?
  103. if current_shortcode.is_empty() {
  104. if text.starts_with("{{") && !text.ends_with("}}") {
  105. current_shortcode += &text;
  106. } else if text.starts_with("{%") && !text.ends_with("%}") {
  107. current_shortcode += &text;
  108. }
  109. } else {
  110. current_shortcode += &text;
  111. }
  112. if current_shortcode.ends_with("}}") || current_shortcode.ends_with("%}") {
  113. text = Owned(current_shortcode.clone());
  114. current_shortcode = String::new();
  115. }
  116. // Shortcode without body
  117. if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) {
  118. let (name, args) = parse_shortcode(&text);
  119. added_shortcode = true;
  120. match render_simple_shortcode(context.tera, &name, &args) {
  121. // Make before and after cleaning up of extra <p> / </p> tags more parallel.
  122. // Or, in other words:
  123. // TERRIBLE HORRIBLE NO GOOD VERY BAD HACK
  124. Ok(s) => return Event::Html(Owned(format!("</p>{}<p>", s))),
  125. Err(e) => {
  126. error = Some(e);
  127. return Event::Html(Owned(String::new()));
  128. }
  129. }
  130. }
  131. // Shortcode with a body
  132. if shortcode_block.is_none() && text.starts_with("{%") && text.ends_with("%}") {
  133. if SHORTCODE_RE.is_match(&text) {
  134. let (name, args) = parse_shortcode(&text);
  135. shortcode_block = Some(ShortCode::new(&name, args));
  136. }
  137. // Don't return anything
  138. return Event::Text(Owned(String::new()));
  139. }
  140. // If we have some text while in a shortcode, it's either the body
  141. // or the end tag
  142. if shortcode_block.is_some() {
  143. if let Some(ref mut shortcode) = shortcode_block {
  144. if text.trim() == "{% end %}" {
  145. added_shortcode = true;
  146. clear_shortcode_block = true;
  147. match shortcode.render(context.tera) {
  148. Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
  149. Err(e) => {
  150. error = Some(e);
  151. return Event::Html(Owned(String::new()));
  152. }
  153. }
  154. } else {
  155. shortcode.append(&text);
  156. return Event::Html(Owned(String::new()));
  157. }
  158. }
  159. }
  160. // Business as usual
  161. Event::Text(text)
  162. },
  163. Event::Start(Tag::CodeBlock(ref info)) => {
  164. in_code_block = true;
  165. if !should_highlight {
  166. return Event::Html(Owned("<pre><code>".to_owned()));
  167. }
  168. let theme = &THEME_SET.themes[&context.highlight_theme];
  169. highlighter = SYNTAX_SET.with(|ss| {
  170. let syntax = info
  171. .split(' ')
  172. .next()
  173. .and_then(|lang| ss.find_syntax_by_token(lang))
  174. .unwrap_or_else(|| ss.find_syntax_plain_text());
  175. Some(HighlightLines::new(syntax, theme))
  176. });
  177. let snippet = start_coloured_html_snippet(theme);
  178. Event::Html(Owned(snippet))
  179. },
  180. Event::End(Tag::CodeBlock(_)) => {
  181. in_code_block = false;
  182. if !should_highlight{
  183. return Event::Html(Owned("</code></pre>\n".to_owned()))
  184. }
  185. // reset highlight and close the code block
  186. highlighter = None;
  187. Event::Html(Owned("</pre>".to_owned()))
  188. },
  189. // Need to handle relative links
  190. Event::Start(Tag::Link(ref link, ref title)) => {
  191. if in_header {
  192. return Event::Html(Owned("".to_owned()));
  193. }
  194. if link.starts_with("./") {
  195. match resolve_internal_link(link, context.permalinks) {
  196. Ok(url) => {
  197. return Event::Start(Tag::Link(Owned(url), title.clone()));
  198. },
  199. Err(_) => {
  200. error = Some(format!("Relative link {} not found.", link).into());
  201. return Event::Html(Owned("".to_string()));
  202. }
  203. };
  204. }
  205. Event::Start(Tag::Link(link.clone(), title.clone()))
  206. },
  207. Event::End(Tag::Link(_, _)) => {
  208. if in_header {
  209. return Event::Html(Owned("".to_owned()));
  210. }
  211. event
  212. }
  213. // need to know when we are in a code block to disable shortcodes in them
  214. Event::Start(Tag::Code) => {
  215. in_code_block = true;
  216. if in_header {
  217. temp_header.push("<code>");
  218. return Event::Html(Owned(String::new()));
  219. }
  220. event
  221. },
  222. Event::End(Tag::Code) => {
  223. in_code_block = false;
  224. if in_header {
  225. temp_header.push("</code>");
  226. return Event::Html(Owned(String::new()));
  227. }
  228. event
  229. },
  230. Event::Start(Tag::Header(num)) => {
  231. in_header = true;
  232. temp_header = TempHeader::new(num);
  233. Event::Html(Owned(String::new()))
  234. },
  235. Event::End(Tag::Header(_)) => {
  236. // End of a header, reset all the things and return the stringified version of the header
  237. in_header = false;
  238. header_created = false;
  239. let val = temp_header.to_string(context);
  240. headers.push(temp_header.clone());
  241. temp_header = TempHeader::default();
  242. Event::Html(Owned(val))
  243. },
  244. // If we added shortcodes, don't close a paragraph since there's none
  245. Event::End(Tag::Paragraph) => {
  246. if added_shortcode {
  247. added_shortcode = false;
  248. return Event::Html(Owned("".to_owned()));
  249. }
  250. event
  251. },
  252. // Ignore softbreaks inside shortcodes
  253. Event::SoftBreak => {
  254. if shortcode_block.is_some() {
  255. return Event::Html(Owned("".to_owned()));
  256. }
  257. event
  258. },
  259. _ => {
  260. // println!("event = {:?}", event);
  261. event
  262. },
  263. }});
  264. cmark::html::push_html(&mut html, parser);
  265. }
  266. if !current_shortcode.is_empty() {
  267. return Err(format!("A shortcode was not closed properly:\n{:?}", current_shortcode).into());
  268. }
  269. match error {
  270. Some(e) => Err(e),
  271. None => Ok((html.replace("<p></p>", "").replace("</p></p>", "</p>"), make_table_of_contents(&headers))),
  272. }
  273. }