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.

252 lines
10KB

  1. use std::borrow::Cow::{Borrowed, Owned};
  2. use self::cmark::{Event, Options, Parser, Tag};
  3. use pulldown_cmark as cmark;
  4. use slug::slugify;
  5. use syntect::easy::HighlightLines;
  6. use syntect::html::{
  7. start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground,
  8. };
  9. use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET};
  10. use errors::Result;
  11. use link_checker::check_url;
  12. use utils::site::resolve_internal_link;
  13. use context::RenderContext;
  14. use table_of_contents::{make_table_of_contents, Header, TempHeader};
  15. const CONTINUE_READING: &str = "<p><a name=\"continue-reading\"></a></p>\n";
  16. #[derive(Debug)]
  17. pub struct Rendered {
  18. pub body: String,
  19. pub summary_len: Option<usize>,
  20. pub toc: Vec<Header>,
  21. }
  22. // We might have cases where the slug is already present in our list of anchor
  23. // for example an article could have several titles named Example
  24. // We add a counter after the slug if the slug is already present, which
  25. // means we will have example, example-1, example-2 etc
  26. fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
  27. if level == 0 && !anchors.contains(&name) {
  28. return name;
  29. }
  30. let new_anchor = format!("{}-{}", name, level + 1);
  31. if !anchors.contains(&new_anchor) {
  32. return new_anchor;
  33. }
  34. find_anchor(anchors, name, level + 1)
  35. }
  36. // Colocated asset links refers to the files in the same directory,
  37. // there it should be a filename only
  38. fn is_colocated_asset_link(link: &str) -> bool {
  39. !link.contains('/') // http://, ftp://, ../ etc
  40. && !link.starts_with("mailto:")
  41. }
  42. pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Rendered> {
  43. // the rendered html
  44. let mut html = String::with_capacity(content.len());
  45. // Set while parsing
  46. let mut error = None;
  47. let mut background = IncludeBackground::Yes;
  48. let mut highlighter: Option<(HighlightLines, bool)> = None;
  49. // If we get text in header, we need to insert the id and a anchor
  50. let mut in_header = false;
  51. // pulldown_cmark can send several text events for a title if there are markdown
  52. // specific characters like `!` in them. We only want to insert the anchor the first time
  53. let mut header_created = false;
  54. let mut anchors: Vec<String> = vec![];
  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. let mut has_summary = false;
  61. opts.insert(Options::ENABLE_TABLES);
  62. opts.insert(Options::ENABLE_FOOTNOTES);
  63. {
  64. let parser = Parser::new_ext(content, opts).map(|event| {
  65. match event {
  66. Event::Text(text) => {
  67. // Header first
  68. if in_header {
  69. if header_created {
  70. temp_header.add_text(&text);
  71. return Event::Html(Borrowed(""));
  72. }
  73. // += as we might have some <code> or other things already there
  74. temp_header.add_text(&text);
  75. header_created = true;
  76. return Event::Html(Borrowed(""));
  77. }
  78. // if we are in the middle of a code block
  79. if let Some((ref mut highlighter, in_extra)) = highlighter {
  80. let highlighted = if in_extra {
  81. if let Some(ref extra) = context.config.extra_syntax_set {
  82. highlighter.highlight(&text, &extra)
  83. } else {
  84. unreachable!("Got a highlighter from extra syntaxes but no extra?");
  85. }
  86. } else {
  87. highlighter.highlight(&text, &SYNTAX_SET)
  88. };
  89. //let highlighted = &highlighter.highlight(&text, ss);
  90. let html = styled_line_to_highlighted_html(&highlighted, background);
  91. return Event::Html(Owned(html));
  92. }
  93. // Business as usual
  94. Event::Text(text)
  95. }
  96. Event::Start(Tag::CodeBlock(ref info)) => {
  97. if !context.config.highlight_code {
  98. return Event::Html(Borrowed("<pre><code>"));
  99. }
  100. let theme = &THEME_SET.themes[&context.config.highlight_theme];
  101. highlighter = Some(get_highlighter(info, &context.config));
  102. // This selects the background color the same way that start_coloured_html_snippet does
  103. let color =
  104. theme.settings.background.unwrap_or(::syntect::highlighting::Color::WHITE);
  105. background = IncludeBackground::IfDifferent(color);
  106. let snippet = start_highlighted_html_snippet(theme);
  107. Event::Html(Owned(snippet.0))
  108. }
  109. Event::End(Tag::CodeBlock(_)) => {
  110. if !context.config.highlight_code {
  111. return Event::Html(Borrowed("</code></pre>\n"));
  112. }
  113. // reset highlight and close the code block
  114. highlighter = None;
  115. Event::Html(Borrowed("</pre>"))
  116. }
  117. Event::Start(Tag::Image(src, title)) => {
  118. if is_colocated_asset_link(&src) {
  119. return Event::Start(Tag::Image(
  120. Owned(format!("{}{}", context.current_page_permalink, src)),
  121. title,
  122. ));
  123. }
  124. Event::Start(Tag::Image(src, title))
  125. }
  126. Event::Start(Tag::Link(link, title)) => {
  127. // A few situations here:
  128. // - it could be a relative link (starting with `./`)
  129. // - it could be a link to a co-located asset
  130. // - it could be a normal link
  131. // - any of those can be in a header or not: if it's in a header
  132. // we need to append to a string
  133. let fixed_link = if link.starts_with("./") {
  134. match resolve_internal_link(&link, context.permalinks) {
  135. Ok(url) => url,
  136. Err(_) => {
  137. error = Some(format!("Relative link {} not found.", link).into());
  138. return Event::Html(Borrowed(""));
  139. }
  140. }
  141. } else if is_colocated_asset_link(&link) {
  142. format!("{}{}", context.current_page_permalink, link)
  143. } else if context.config.check_external_links
  144. && !link.starts_with('#')
  145. && !link.starts_with("mailto:")
  146. {
  147. let res = check_url(&link);
  148. if res.is_valid() {
  149. link.to_string()
  150. } else {
  151. error = Some(
  152. format!("Link {} is not valid: {}", link, res.message()).into(),
  153. );
  154. String::new()
  155. }
  156. } else {
  157. link.to_string()
  158. };
  159. if in_header {
  160. let html = if title.is_empty() {
  161. format!("<a href=\"{}\">", fixed_link)
  162. } else {
  163. format!("<a href=\"{}\" title=\"{}\">", fixed_link, title)
  164. };
  165. temp_header.add_html(&html);
  166. return Event::Html(Borrowed(""));
  167. }
  168. Event::Start(Tag::Link(Owned(fixed_link), title))
  169. }
  170. Event::End(Tag::Link(_, _)) => {
  171. if in_header {
  172. temp_header.add_html("</a>");
  173. return Event::Html(Borrowed(""));
  174. }
  175. event
  176. }
  177. Event::Start(Tag::Code) => {
  178. if in_header {
  179. temp_header.add_html("<code>");
  180. return Event::Html(Borrowed(""));
  181. }
  182. event
  183. }
  184. Event::End(Tag::Code) => {
  185. if in_header {
  186. temp_header.add_html("</code>");
  187. return Event::Html(Borrowed(""));
  188. }
  189. event
  190. }
  191. Event::Start(Tag::Header(num)) => {
  192. in_header = true;
  193. temp_header = TempHeader::new(num);
  194. Event::Html(Borrowed(""))
  195. }
  196. Event::End(Tag::Header(_)) => {
  197. // End of a header, reset all the things and return the header string
  198. let id = find_anchor(&anchors, slugify(&temp_header.title), 0);
  199. anchors.push(id.clone());
  200. temp_header.permalink = format!("{}#{}", context.current_page_permalink, id);
  201. temp_header.id = id;
  202. in_header = false;
  203. header_created = false;
  204. let val = temp_header.to_string(context.tera, context.insert_anchor);
  205. headers.push(temp_header.clone());
  206. temp_header = TempHeader::default();
  207. Event::Html(Owned(val))
  208. }
  209. Event::Html(ref markup) if markup.contains("<!-- more -->") => {
  210. has_summary = true;
  211. Event::Html(Borrowed(CONTINUE_READING))
  212. }
  213. _ => event,
  214. }
  215. });
  216. cmark::html::push_html(&mut html, parser);
  217. }
  218. if let Some(e) = error {
  219. return Err(e);
  220. } else {
  221. Ok(Rendered {
  222. summary_len: if has_summary { html.find(CONTINUE_READING) } else { None },
  223. body: html,
  224. toc: make_table_of_contents(&headers),
  225. })
  226. }
  227. }