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.

173 lines
7.0KB

  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 highlighting::{get_highlighter, THEME_SET};
  10. use table_of_contents::{TempHeader, Header, make_table_of_contents};
  11. use context::RenderContext;
  12. // We might have cases where the slug is already present in our list of anchor
  13. // for example an article could have several titles named Example
  14. // We add a counter after the slug if the slug is already present, which
  15. // means we will have example, example-1, example-2 etc
  16. fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
  17. if level == 0 && !anchors.contains(&name) {
  18. return name.to_string();
  19. }
  20. let new_anchor = format!("{}-{}", name, level + 1);
  21. if !anchors.contains(&new_anchor) {
  22. return new_anchor;
  23. }
  24. find_anchor(anchors, name, level + 1)
  25. }
  26. pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> {
  27. // the rendered html
  28. let mut html = String::new();
  29. // Set while parsing
  30. let mut error = None;
  31. let mut highlighter: Option<HighlightLines> = None;
  32. // If we get text in header, we need to insert the id and a anchor
  33. let mut in_header = false;
  34. // pulldown_cmark can send several text events for a title if there are markdown
  35. // specific characters like `!` in them. We only want to insert the anchor the first time
  36. let mut header_created = false;
  37. let mut anchors: Vec<String> = vec![];
  38. let mut headers = vec![];
  39. // Defaults to a 0 level so not a real header
  40. // It should be an Option ideally but not worth the hassle to update
  41. let mut temp_header = TempHeader::default();
  42. let mut opts = Options::empty();
  43. opts.insert(OPTION_ENABLE_TABLES);
  44. opts.insert(OPTION_ENABLE_FOOTNOTES);
  45. {
  46. let parser = Parser::new_ext(content, opts).map(|event| {
  47. match event {
  48. Event::Text(text) => {
  49. // Header first
  50. if in_header {
  51. if header_created {
  52. temp_header.push(&text);
  53. return Event::Html(Owned(String::new()));
  54. }
  55. let id = find_anchor(&anchors, slugify(&text), 0);
  56. anchors.push(id.clone());
  57. // update the header and add it to the list
  58. temp_header.id = id.clone();
  59. // += as we might have some <code> or other things already there
  60. temp_header.title += &text;
  61. temp_header.permalink = format!("{}#{}", context.current_page_permalink, id);
  62. header_created = true;
  63. return Event::Html(Owned(String::new()));
  64. }
  65. // if we are in the middle of a code block
  66. if let Some(ref mut highlighter) = highlighter {
  67. let highlighted = &highlighter.highlight(&text);
  68. let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
  69. return Event::Html(Owned(html));
  70. }
  71. // Business as usual
  72. Event::Text(text)
  73. },
  74. Event::Start(Tag::CodeBlock(ref info)) => {
  75. if !context.config.highlight_code {
  76. return Event::Html(Owned("<pre><code>".to_owned()));
  77. }
  78. let theme = &THEME_SET.themes[&context.config.highlight_theme];
  79. highlighter = Some(get_highlighter(&theme, info));
  80. let snippet = start_coloured_html_snippet(theme);
  81. Event::Html(Owned(snippet))
  82. },
  83. Event::End(Tag::CodeBlock(_)) => {
  84. if !context.config.highlight_code {
  85. return Event::Html(Owned("</code></pre>\n".to_owned()))
  86. }
  87. // reset highlight and close the code block
  88. highlighter = None;
  89. Event::Html(Owned("</pre>".to_owned()))
  90. },
  91. // Need to handle relative links
  92. Event::Start(Tag::Link(ref link, ref title)) => {
  93. if in_header {
  94. return Event::Html(Owned("".to_owned()));
  95. }
  96. if link.starts_with("./") {
  97. match resolve_internal_link(link, context.permalinks) {
  98. Ok(url) => {
  99. return Event::Start(Tag::Link(Owned(url), title.clone()));
  100. },
  101. Err(_) => {
  102. error = Some(format!("Relative link {} not found.", link).into());
  103. return Event::Html(Owned("".to_string()));
  104. }
  105. };
  106. }
  107. Event::Start(Tag::Link(link.clone(), title.clone()))
  108. },
  109. Event::End(Tag::Link(_, _)) => {
  110. if in_header {
  111. return Event::Html(Owned("".to_owned()));
  112. }
  113. event
  114. }
  115. Event::Start(Tag::Code) => {
  116. if in_header {
  117. temp_header.push("<code>");
  118. return Event::Html(Owned(String::new()));
  119. }
  120. event
  121. },
  122. Event::End(Tag::Code) => {
  123. if in_header {
  124. temp_header.push("</code>");
  125. return Event::Html(Owned(String::new()));
  126. }
  127. event
  128. },
  129. Event::Start(Tag::Header(num)) => {
  130. in_header = true;
  131. temp_header = TempHeader::new(num);
  132. Event::Html(Owned(String::new()))
  133. },
  134. Event::End(Tag::Header(_)) => {
  135. // End of a header, reset all the things and return the stringified version of the header
  136. in_header = false;
  137. header_created = false;
  138. let val = temp_header.to_string(context.tera, context.insert_anchor);
  139. headers.push(temp_header.clone());
  140. temp_header = TempHeader::default();
  141. Event::Html(Owned(val))
  142. },
  143. _ => event,
  144. }
  145. });
  146. cmark::html::push_html(&mut html, parser);
  147. }
  148. match error {
  149. Some(e) => Err(e),
  150. None => Ok((
  151. html.replace("<p></p>", "").replace("</p></p>", "</p>"),
  152. make_table_of_contents(&headers)
  153. )),
  154. }
  155. }