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.

167 lines
6.0KB

  1. use std::borrow::Cow::Owned;
  2. use pulldown_cmark as cmark;
  3. use self::cmark::{Parser, Event, Tag};
  4. use syntect::dumps::from_binary;
  5. use syntect::easy::HighlightLines;
  6. use syntect::parsing::SyntaxSet;
  7. use syntect::highlighting::ThemeSet;
  8. use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};
  9. // We need to put those in a struct to impl Send and sync
  10. pub struct Setup {
  11. syntax_set: SyntaxSet,
  12. pub theme_set: ThemeSet,
  13. }
  14. unsafe impl Send for Setup {}
  15. unsafe impl Sync for Setup {}
  16. lazy_static!{
  17. pub static ref SETUP: Setup = Setup {
  18. syntax_set: SyntaxSet::load_defaults_newlines(),
  19. theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump"))
  20. };
  21. }
  22. struct CodeHighlightingParser<'a> {
  23. // The block we're currently highlighting
  24. highlighter: Option<HighlightLines<'a>>,
  25. parser: Parser<'a>,
  26. theme: &'a str,
  27. }
  28. impl<'a> CodeHighlightingParser<'a> {
  29. pub fn new(parser: Parser<'a>, theme: &'a str) -> CodeHighlightingParser<'a> {
  30. CodeHighlightingParser {
  31. highlighter: None,
  32. parser: parser,
  33. theme: theme,
  34. }
  35. }
  36. }
  37. impl<'a> Iterator for CodeHighlightingParser<'a> {
  38. type Item = Event<'a>;
  39. fn next(&mut self) -> Option<Event<'a>> {
  40. // Not using pattern matching to reduce indentation levels
  41. let next_opt = self.parser.next();
  42. if next_opt.is_none() {
  43. return None;
  44. }
  45. let item = next_opt.unwrap();
  46. // Below we just look for the start of a code block and highlight everything
  47. // until we see the end of a code block.
  48. // Everything else happens as normal in pulldown_cmark
  49. match item {
  50. Event::Text(text) => {
  51. // if we are in the middle of a code block
  52. if let Some(ref mut highlighter) = self.highlighter {
  53. let highlighted = &highlighter.highlight(&text);
  54. let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
  55. Some(Event::Html(Owned(html)))
  56. } else {
  57. Some(Event::Text(text))
  58. }
  59. },
  60. Event::Start(Tag::CodeBlock(ref info)) => {
  61. let theme = &SETUP.theme_set.themes[self.theme];
  62. let syntax = info
  63. .split(' ')
  64. .next()
  65. .and_then(|lang| SETUP.syntax_set.find_syntax_by_token(lang))
  66. .unwrap_or_else(|| SETUP.syntax_set.find_syntax_plain_text());
  67. self.highlighter = Some(
  68. HighlightLines::new(syntax, theme)
  69. );
  70. let snippet = start_coloured_html_snippet(theme);
  71. Some(Event::Html(Owned(snippet)))
  72. },
  73. Event::End(Tag::CodeBlock(_)) => {
  74. // reset highlight and close the code block
  75. self.highlighter = None;
  76. Some(Event::Html(Owned("</pre>".to_owned())))
  77. },
  78. _ => Some(item)
  79. }
  80. }
  81. }
  82. pub fn markdown_to_html(content: &str, highlight_code: bool, highlight_theme: &str) -> String {
  83. // We try to be smart about highlighting code as it can be time-consuming
  84. // If the global config disables it, then we do nothing. However,
  85. // if we see a code block in the content, we assume that this page needs
  86. // to be highlighted. It could potentially have false positive if the content
  87. // has ``` in it but that seems kind of unlikely
  88. let should_highlight = if highlight_code {
  89. content.contains("```")
  90. } else {
  91. false
  92. };
  93. let mut html = String::new();
  94. if should_highlight {
  95. let parser = CodeHighlightingParser::new(Parser::new(content), highlight_theme);
  96. cmark::html::push_html(&mut html, parser);
  97. } else {
  98. let parser = Parser::new(content);
  99. cmark::html::push_html(&mut html, parser);
  100. };
  101. html
  102. }
  103. #[cfg(test)]
  104. mod tests {
  105. use super::{markdown_to_html};
  106. #[test]
  107. fn test_markdown_to_html_simple() {
  108. let res = markdown_to_html("# hello", true, "base16-ocean-dark");
  109. assert_eq!(res, "<h1>hello</h1>\n");
  110. }
  111. #[test]
  112. fn test_markdown_to_html_code_block_highlighting_off() {
  113. let res = markdown_to_html("```\n$ gutenberg server\n```", false, "base16-ocean-dark");
  114. assert_eq!(
  115. res,
  116. "<pre><code>$ gutenberg server\n</code></pre>\n"
  117. );
  118. }
  119. #[test]
  120. fn test_markdown_to_html_code_block_no_lang() {
  121. let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true, "base16-ocean-dark");
  122. assert_eq!(
  123. res,
  124. "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>"
  125. );
  126. }
  127. #[test]
  128. fn test_markdown_to_html_code_block_with_lang() {
  129. let res = markdown_to_html("```python\nlist.append(1)\n```", true, "base16-ocean-dark");
  130. assert_eq!(
  131. res,
  132. "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>"
  133. );
  134. }
  135. #[test]
  136. fn test_markdown_to_html_code_block_with_unknown_lang() {
  137. let res = markdown_to_html("```yolo\nlist.append(1)\n```", true, "base16-ocean-dark");
  138. // defaults to plain text
  139. assert_eq!(
  140. res,
  141. "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
  142. );
  143. }
  144. }