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.

241 lines
9.8KB

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