use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag};
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("theme '{0}' is not avaiable")]
InvalidTheme(String),
#[error("error highlighting code")]
HighlightError(#[from] syntect::Error),
}
pub struct PulldownHighlighter {
syntaxset: SyntaxSet,
themeset: ThemeSet,
theme: String,
}
impl PulldownHighlighter {
pub fn new(theme: &str) -> Result<PulldownHighlighter, Error> {
let syntaxset = SyntaxSet::load_defaults_newlines();
let themeset = ThemeSet::load_defaults();
themeset
.themes
.get(theme)
.ok_or(Error::InvalidTheme(theme.to_string()))?;
Ok(PulldownHighlighter {
syntaxset,
themeset,
theme: theme.to_string(),
})
}
pub fn highlight<'a, It>(&self, events: It) -> Result<Vec<Event<'a>>, Error>
where
It: Iterator<Item = Event<'a>>,
{
let mut in_code_block = false;
let mut syntax = self.syntaxset.find_syntax_plain_text();
let theme = self
.themeset
.themes
.get(&self.theme)
.ok_or(Error::InvalidTheme(self.theme.clone()))?;
let mut to_highlight = String::new();
let mut out_events = Vec::new();
for event in events {
match event {
Event::Start(Tag::CodeBlock(kind)) => {
match kind {
CodeBlockKind::Fenced(lang) => {
syntax = self.syntaxset.find_syntax_by_token(&lang).unwrap_or(syntax)
}
CodeBlockKind::Indented => {}
}
in_code_block = true;
}
Event::End(Tag::CodeBlock(_)) => {
if !in_code_block {
panic!("this should never happen");
}
let html =
highlighted_html_for_string(&to_highlight, &self.syntaxset, syntax, theme)?;
to_highlight.clear();
in_code_block = false;
out_events.push(Event::Html(CowStr::from(html)));
}
Event::Text(t) => {
if in_code_block {
to_highlight.push_str(&t);
} else {
out_events.push(Event::Text(t));
}
}
e => {
out_events.push(e);
}
}
}
Ok(out_events)
}
}
pub fn highlight_with_theme<'a, It>(events: It, theme: &str) -> Result<Vec<Event<'a>>, Error>
where
It: Iterator<Item = Event<'a>>,
{
let highlighter = PulldownHighlighter::new(theme)?;
highlighter.highlight(events)
}
pub fn highlight<'a, It>(events: It) -> Result<Vec<Event<'a>>, Error>
where
It: Iterator<Item = Event<'a>>,
{
highlight_with_theme(events, "base16-ocean.dark")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn without_theme() {
let markdown = r#"
```python
print("foo", 42)
```
"#;
let events = pulldown_cmark::Parser::new(markdown);
let events = highlight(events).unwrap();
let mut html = String::new();
pulldown_cmark::html::push_html(&mut html, events.into_iter());
let expected = r#"<pre style="background-color:#2b303b;">
<span style="color:#c0c5ce;"> ```python
</span><span style="color:#c0c5ce;"> print("foo", 42)
</span><span style="color:#c0c5ce;"> ```
</span></pre>
"#;
assert_eq!(html, expected);
}
#[test]
fn with_theme() {
let markdown = r#"```python
print("foo", 42)
```
"#;
let events = pulldown_cmark::Parser::new(markdown);
let events = highlight_with_theme(events, "Solarized (dark)").unwrap();
let mut html = String::new();
pulldown_cmark::html::push_html(&mut html, events.into_iter());
let expected = r#"<pre style="background-color:#002b36;">
<span style="color:#859900;">print</span><span style="color:#657b83;">(</span><span style="color:#839496;">"</span><span style="color:#2aa198;">foo</span><span style="color:#839496;">", </span><span style="color:#6c71c4;">42</span><span style="color:#657b83;">)
</span></pre>
"#;
assert_eq!(html, expected);
}
}