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.

155 lines
5.5KB

  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. let mut html = String::new();
  84. if highlight_code {
  85. let parser = CodeHighlightingParser::new(Parser::new(content), highlight_theme);
  86. cmark::html::push_html(&mut html, parser);
  87. } else {
  88. let parser = Parser::new(content);
  89. cmark::html::push_html(&mut html, parser);
  90. };
  91. html
  92. }
  93. #[cfg(test)]
  94. mod tests {
  95. use super::{markdown_to_html};
  96. #[test]
  97. fn test_markdown_to_html_simple() {
  98. let res = markdown_to_html("# hello", true, "base16-ocean-dark");
  99. assert_eq!(res, "<h1>hello</h1>\n");
  100. }
  101. #[test]
  102. fn test_markdown_to_html_code_block_highlighting_off() {
  103. let res = markdown_to_html("```\n$ gutenberg server\n```", false, "base16-ocean-dark");
  104. assert_eq!(
  105. res,
  106. "<pre><code>$ gutenberg server\n</code></pre>\n"
  107. );
  108. }
  109. #[test]
  110. fn test_markdown_to_html_code_block_no_lang() {
  111. let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true, "base16-ocean-dark");
  112. assert_eq!(
  113. res,
  114. "<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>"
  115. );
  116. }
  117. #[test]
  118. fn test_markdown_to_html_code_block_with_lang() {
  119. let res = markdown_to_html("```python\nlist.append(1)\n```", true, "base16-ocean-dark");
  120. assert_eq!(
  121. res,
  122. "<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>"
  123. );
  124. }
  125. #[test]
  126. fn test_markdown_to_html_code_block_with_unknown_lang() {
  127. let res = markdown_to_html("```yolo\nlist.append(1)\n```", true, "base16-ocean-dark");
  128. // defaults to plain text
  129. assert_eq!(
  130. res,
  131. "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
  132. );
  133. }
  134. }