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.

261 lines
11KB

  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. let mut shortcode_block = None;
  28. // shortcodes live outside of paragraph so we need to ensure we don't close
  29. // a paragraph that has already been closed
  30. let mut added_shortcode = false;
  31. // Don't transform things that look like shortcodes in code blocks
  32. let mut in_code_block = false;
  33. // If we get text in header, we need to insert the id and a anchor
  34. let mut in_header = false;
  35. // pulldown_cmark can send several text events for a title if there are markdown
  36. // specific characters like `!` in them. We only want to insert the anchor the first time
  37. let mut header_created = false;
  38. let mut anchors: Vec<String> = vec![];
  39. // the rendered html
  40. let mut html = String::new();
  41. // We might have cases where the slug is already present in our list of anchor
  42. // for example an article could have several titles named Example
  43. // We add a counter after the slug if the slug is already present, which
  44. // means we will have example, example-1, example-2 etc
  45. fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
  46. if level == 0 && !anchors.contains(&name) {
  47. return name.to_string();
  48. }
  49. let new_anchor = format!("{}-{}", name, level + 1);
  50. if !anchors.contains(&new_anchor) {
  51. return new_anchor;
  52. }
  53. find_anchor(anchors, name, level + 1)
  54. }
  55. let mut headers = vec![];
  56. // Defaults to a 0 level so not a real header
  57. // It should be an Option ideally but not worth the hassle to update
  58. let mut temp_header = TempHeader::default();
  59. let mut opts = Options::empty();
  60. opts.insert(OPTION_ENABLE_TABLES);
  61. opts.insert(OPTION_ENABLE_FOOTNOTES);
  62. {
  63. let parser = Parser::new_ext(content, opts).map(|event| match event {
  64. Event::Text(text) => {
  65. // Header first
  66. if in_header {
  67. if header_created {
  68. temp_header.push(&text);
  69. return Event::Html(Owned(String::new()));
  70. }
  71. let id = find_anchor(&anchors, slugify(&text), 0);
  72. anchors.push(id.clone());
  73. // update the header and add it to the list
  74. temp_header.id = id.clone();
  75. // += as we might have some <code> or other things already there
  76. temp_header.title += &text;
  77. temp_header.permalink = format!("{}#{}", context.current_page_permalink, id);
  78. header_created = true;
  79. return Event::Html(Owned(String::new()));
  80. }
  81. // if we are in the middle of a code block
  82. if let Some(ref mut highlighter) = highlighter {
  83. let highlighted = &highlighter.highlight(&text);
  84. let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
  85. return Event::Html(Owned(html));
  86. }
  87. if in_code_block {
  88. return Event::Text(text);
  89. }
  90. // Shortcode without body
  91. if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) {
  92. let (name, args) = parse_shortcode(&text);
  93. added_shortcode = true;
  94. match render_simple_shortcode(context.tera, &name, &args) {
  95. Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
  96. Err(e) => {
  97. error = Some(e);
  98. return Event::Html(Owned(String::new()));
  99. }
  100. }
  101. }
  102. // Shortcode with a body
  103. if shortcode_block.is_none() && text.starts_with("{%") && text.ends_with("%}") {
  104. if SHORTCODE_RE.is_match(&text) {
  105. let (name, args) = parse_shortcode(&text);
  106. shortcode_block = Some(ShortCode::new(&name, args));
  107. }
  108. // Don't return anything
  109. return Event::Text(Owned(String::new()));
  110. }
  111. // If we have some text while in a shortcode, it's either the body
  112. // or the end tag
  113. if shortcode_block.is_some() {
  114. if let Some(ref mut shortcode) = shortcode_block {
  115. if text.trim() == "{% end %}" {
  116. added_shortcode = true;
  117. match shortcode.render(context.tera) {
  118. Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
  119. Err(e) => {
  120. error = Some(e);
  121. return Event::Html(Owned(String::new()));
  122. }
  123. }
  124. } else {
  125. shortcode.append(&text);
  126. return Event::Html(Owned(String::new()));
  127. }
  128. }
  129. }
  130. // Business as usual
  131. Event::Text(text)
  132. },
  133. Event::Start(Tag::CodeBlock(ref info)) => {
  134. in_code_block = true;
  135. if !should_highlight {
  136. return Event::Html(Owned("<pre><code>".to_owned()));
  137. }
  138. let theme = &THEME_SET.themes[&context.highlight_theme];
  139. highlighter = SYNTAX_SET.with(|ss| {
  140. let syntax = info
  141. .split(' ')
  142. .next()
  143. .and_then(|lang| ss.find_syntax_by_token(lang))
  144. .unwrap_or_else(|| ss.find_syntax_plain_text());
  145. Some(HighlightLines::new(syntax, theme))
  146. });
  147. let snippet = start_coloured_html_snippet(theme);
  148. Event::Html(Owned(snippet))
  149. },
  150. Event::End(Tag::CodeBlock(_)) => {
  151. in_code_block = false;
  152. if !should_highlight{
  153. return Event::Html(Owned("</code></pre>\n".to_owned()))
  154. }
  155. // reset highlight and close the code block
  156. highlighter = None;
  157. Event::Html(Owned("</pre>".to_owned()))
  158. },
  159. // Need to handle relative links
  160. Event::Start(Tag::Link(ref link, ref title)) => {
  161. if in_header {
  162. return Event::Html(Owned("".to_owned()));
  163. }
  164. if link.starts_with("./") {
  165. match resolve_internal_link(link, context.permalinks) {
  166. Ok(url) => {
  167. return Event::Start(Tag::Link(Owned(url), title.clone()));
  168. },
  169. Err(_) => {
  170. error = Some(format!("Relative link {} not found.", link).into());
  171. return Event::Html(Owned("".to_string()));
  172. }
  173. };
  174. }
  175. Event::Start(Tag::Link(link.clone(), title.clone()))
  176. },
  177. Event::End(Tag::Link(_, _)) => {
  178. if in_header {
  179. return Event::Html(Owned("".to_owned()));
  180. }
  181. event
  182. }
  183. // need to know when we are in a code block to disable shortcodes in them
  184. Event::Start(Tag::Code) => {
  185. in_code_block = true;
  186. if in_header {
  187. temp_header.push("<code>");
  188. return Event::Html(Owned(String::new()));
  189. }
  190. event
  191. },
  192. Event::End(Tag::Code) => {
  193. in_code_block = false;
  194. if in_header {
  195. temp_header.push("</code>");
  196. return Event::Html(Owned(String::new()));
  197. }
  198. event
  199. },
  200. Event::Start(Tag::Header(num)) => {
  201. in_header = true;
  202. temp_header = TempHeader::new(num);
  203. Event::Html(Owned(String::new()))
  204. },
  205. Event::End(Tag::Header(_)) => {
  206. // End of a header, reset all the things and return the stringified version of the header
  207. in_header = false;
  208. header_created = false;
  209. let val = temp_header.to_string(context);
  210. headers.push(temp_header.clone());
  211. temp_header = TempHeader::default();
  212. Event::Html(Owned(val))
  213. },
  214. // If we added shortcodes, don't close a paragraph since there's none
  215. Event::End(Tag::Paragraph) => {
  216. if added_shortcode {
  217. added_shortcode = false;
  218. return Event::Html(Owned("".to_owned()));
  219. }
  220. event
  221. },
  222. // Ignore softbreaks inside shortcodes
  223. Event::SoftBreak => {
  224. if shortcode_block.is_some() {
  225. return Event::Html(Owned("".to_owned()));
  226. }
  227. event
  228. },
  229. _ => {
  230. // println!("event = {:?}", event);
  231. event
  232. },
  233. });
  234. cmark::html::push_html(&mut html, parser);
  235. }
  236. match error {
  237. Some(e) => Err(e),
  238. None => Ok((html.replace("<p></p>", ""), make_table_of_contents(&headers))),
  239. }
  240. }