From a99f084ee29cb411a429bbfe1149d89fb5f19865 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 7 Mar 2017 21:34:31 +0900 Subject: [PATCH] Code highlighting --- src/cmd/serve.rs | 4 +- src/config.rs | 2 +- src/main.rs | 1 + src/markdown.rs | 150 +++++++++++++++++++++++++++++++++++++++++++++++ src/page.rs | 13 +--- 5 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/markdown.rs diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 6bca8e3..9c8c15a 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -33,7 +33,9 @@ pub fn serve(interface: &str, port: &str) -> Result<()> { let mut mount = Mount::new(); mount.mount("/", Static::new(Path::new("public/"))); mount.mount("/livereload.js", livereload_handler); - let server = Iron::new(mount).http(address.clone()).unwrap(); + // Starts with a _ to not trigger the unused lint + // we need to assign to a variable otherwise it will block + let _iron = Iron::new(mount).http(address.clone()).unwrap(); println!("Web server is available at http://{}", address); println!("Press CTRL+C to stop"); diff --git a/src/config.rs b/src/config.rs index 6f51482..d7e0c25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use toml::{Value as Toml, self}; use errors::{Result, ResultExt}; -// TODO: disable tag(s)/category(ies) page generation + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Config { /// Title of the site diff --git a/src/main.rs b/src/main.rs index 37e8f0c..bf4f79f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ mod cmd; mod page; mod front_matter; mod site; +mod markdown; fn main() { diff --git a/src/markdown.rs b/src/markdown.rs new file mode 100644 index 0000000..f1fe3bb --- /dev/null +++ b/src/markdown.rs @@ -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>, + 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> { + // 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("".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, "

hello

\n"); + } + + #[test] + fn test_markdown_to_html_code_block_highlighting_off() { + let res = markdown_to_html("```\n$ gutenberg server\n```", false); + 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); + 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); + 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); + // defaults to plain text + assert_eq!( + res, + "
\nlist.append(1)\n
" + ); + } +} diff --git a/src/page.rs b/src/page.rs index a4c4d9c..0722c82 100644 --- a/src/page.rs +++ b/src/page.rs @@ -6,7 +6,6 @@ use std::path::Path; use std::result::Result as StdResult; -use pulldown_cmark as cmark; use regex::Regex; use tera::{Tera, Context}; use serde::ser::{SerializeStruct, self}; @@ -15,6 +14,7 @@ use slug::slugify; use errors::{Result, ResultExt}; use config::Config; use front_matter::{FrontMatter}; +use markdown::markdown_to_html; lazy_static! { @@ -22,13 +22,6 @@ lazy_static! { static ref SUMMARY_RE: Regex = Regex::new(r"").unwrap(); } -fn markdown_to_html(content: &str) -> String { - let mut html = String::new(); - let parser = cmark::Parser::new(content); - cmark::html::push_html(&mut html, parser); - html -} - #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct Page { @@ -120,13 +113,13 @@ impl Page { let mut page = Page::new(meta); page.filepath = filepath.to_string(); page.raw_content = content.to_string(); - page.content = markdown_to_html(&page.raw_content); + page.content = markdown_to_html(&page.raw_content, config.highlight_code.unwrap()); if page.raw_content.contains("") { page.summary = { let summary = SUMMARY_RE.split(&page.raw_content).collect::>()[0]; - markdown_to_html(summary) + markdown_to_html(summary, config.highlight_code.unwrap()) } }