|
|
@@ -0,0 +1,150 @@ |
|
|
|
use std::borrow::Cow::Owned; |
|
|
|
|
|
|
|
use pulldown_cmark as cmark; |
|
|
|
use self::cmark::{Parser, Event, Tag}; |
|
|
|
|
|
|
|
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 |
|
|
|
struct Setup { |
|
|
|
syntax_set: SyntaxSet, |
|
|
|
theme_set: ThemeSet, |
|
|
|
} |
|
|
|
|
|
|
|
unsafe impl Send for Setup {} |
|
|
|
unsafe impl Sync for Setup {} |
|
|
|
|
|
|
|
lazy_static!{ |
|
|
|
static ref SETUP: Setup = Setup { |
|
|
|
syntax_set: SyntaxSet::load_defaults_newlines(), |
|
|
|
theme_set: ThemeSet::load_defaults() |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
struct CodeHighlightingParser<'a> { |
|
|
|
// The block we're currently highlighting |
|
|
|
highlighter: Option<HighlightLines<'a>>, |
|
|
|
parser: Parser<'a>, |
|
|
|
} |
|
|
|
|
|
|
|
impl<'a> CodeHighlightingParser<'a> { |
|
|
|
pub fn new(parser: Parser<'a>) -> CodeHighlightingParser<'a> { |
|
|
|
CodeHighlightingParser { |
|
|
|
highlighter: None, |
|
|
|
parser: parser, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
impl<'a> Iterator for CodeHighlightingParser<'a> { |
|
|
|
type Item = Event<'a>; |
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Event<'a>> { |
|
|
|
// 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 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, &SETUP.theme_set.themes["base16-ocean.dark"]) |
|
|
|
); |
|
|
|
let snippet = start_coloured_html_snippet(&SETUP.theme_set.themes["base16-ocean.dark"]); |
|
|
|
Some(Event::Html(Owned(snippet))) |
|
|
|
}, |
|
|
|
Event::End(Tag::CodeBlock(_)) => { |
|
|
|
// reset highlight and close the code block |
|
|
|
self.highlighter = None; |
|
|
|
Some(Event::Html(Owned("</pre>".to_owned()))) |
|
|
|
}, |
|
|
|
_ => Some(item) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn markdown_to_html(content: &str, highlight_code: bool) -> String { |
|
|
|
let mut html = String::new(); |
|
|
|
if highlight_code { |
|
|
|
let parser = CodeHighlightingParser::new(Parser::new(content)); |
|
|
|
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); |
|
|
|
assert_eq!(res, "<h1>hello</h1>\n"); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn test_markdown_to_html_code_block_highlighting_off() { |
|
|
|
let res = markdown_to_html("```\n$ gutenberg server\n```", false); |
|
|
|
assert_eq!( |
|
|
|
res, |
|
|
|
"<pre><code>$ gutenberg server\n</code></pre>\n" |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn test_markdown_to_html_code_block_no_lang() { |
|
|
|
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true); |
|
|
|
assert_eq!( |
|
|
|
res, |
|
|
|
"<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>" |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn test_markdown_to_html_code_block_with_lang() { |
|
|
|
let res = markdown_to_html("```python\nlist.append(1)\n```", true); |
|
|
|
assert_eq!( |
|
|
|
res, |
|
|
|
"<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>" |
|
|
|
); |
|
|
|
} |
|
|
|
#[test] |
|
|
|
fn test_markdown_to_html_code_block_with_unknown_lang() { |
|
|
|
let res = markdown_to_html("```yolo\nlist.append(1)\n```", true); |
|
|
|
// defaults to plain text |
|
|
|
assert_eq!( |
|
|
|
res, |
|
|
|
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>" |
|
|
|
); |
|
|
|
} |
|
|
|
} |