use std::borrow::Cow::Owned; use pulldown_cmark as cmark; use self::cmark::{Parser, Event, Tag}; use syntect::dumps::from_binary; use syntect::easy::HighlightLines; use syntect::parsing::SyntaxSet; use syntect::highlighting::ThemeSet; use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; // We need to put those in a struct to impl Send and sync pub struct Setup { syntax_set: SyntaxSet, pub theme_set: ThemeSet, } unsafe impl Send for Setup {} unsafe impl Sync for Setup {} lazy_static!{ pub static ref SETUP: Setup = Setup { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump")) }; } struct CodeHighlightingParser<'a> { // The block we're currently highlighting highlighter: Option>, parser: Parser<'a>, theme: &'a str, } impl<'a> CodeHighlightingParser<'a> { pub fn new(parser: Parser<'a>, theme: &'a str) -> CodeHighlightingParser<'a> { CodeHighlightingParser { highlighter: None, parser: parser, theme: theme, } } } impl<'a> Iterator for CodeHighlightingParser<'a> { type Item = Event<'a>; fn next(&mut self) -> Option> { // Not using pattern matching to reduce indentation levels let next_opt = self.parser.next(); if next_opt.is_none() { return None; } let item = next_opt.unwrap(); // Below we just look for the start of a code block and highlight everything // until we see the end of a code block. // Everything else happens as normal in pulldown_cmark match item { Event::Text(text) => { // if we are in the middle of a code block if let Some(ref mut highlighter) = self.highlighter { let highlighted = &highlighter.highlight(&text); let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes); Some(Event::Html(Owned(html))) } else { Some(Event::Text(text)) } }, Event::Start(Tag::CodeBlock(ref info)) => { let theme = &SETUP.theme_set.themes[self.theme]; let syntax = info .split(' ') .next() .and_then(|lang| SETUP.syntax_set.find_syntax_by_token(lang)) .unwrap_or_else(|| SETUP.syntax_set.find_syntax_plain_text()); self.highlighter = Some( HighlightLines::new(syntax, &theme) ); let snippet = start_coloured_html_snippet(&theme); Some(Event::Html(Owned(snippet))) }, Event::End(Tag::CodeBlock(_)) => { // reset highlight and close the code block self.highlighter = None; Some(Event::Html(Owned("".to_owned()))) }, _ => Some(item) } } } pub fn markdown_to_html(content: &str, highlight_code: bool, highlight_theme: &str) -> String { let mut html = String::new(); if highlight_code { let parser = CodeHighlightingParser::new(Parser::new(content), highlight_theme); cmark::html::push_html(&mut html, parser); } else { let parser = Parser::new(content); cmark::html::push_html(&mut html, parser); }; html } #[cfg(test)] mod tests { use super::{markdown_to_html}; #[test] fn test_markdown_to_html_simple() { let res = markdown_to_html("# hello", true, "base16-ocean-dark"); assert_eq!(res, "

hello

\n"); } #[test] fn test_markdown_to_html_code_block_highlighting_off() { let res = markdown_to_html("```\n$ gutenberg server\n```", false, "base16-ocean-dark"); assert_eq!( res, "
$ gutenberg server\n
\n" ); } #[test] fn test_markdown_to_html_code_block_no_lang() { let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true, "base16-ocean-dark"); assert_eq!( res, "
\n$ gutenberg server\n$ ping\n
" ); } #[test] fn test_markdown_to_html_code_block_with_lang() { let res = markdown_to_html("```python\nlist.append(1)\n```", true, "base16-ocean-dark"); assert_eq!( res, "
\nlist.append(1)\n
" ); } #[test] fn test_markdown_to_html_code_block_with_unknown_lang() { let res = markdown_to_html("```yolo\nlist.append(1)\n```", true, "base16-ocean-dark"); // defaults to plain text assert_eq!( res, "
\nlist.append(1)\n
" ); } }