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.

151 lines
5.2KB

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