use crate::config::EmbeddedContentErrorPolicy;
#[cfg(any(feature = "mermaid", feature = "latex"))]
use crate::error::NoteError;
use pulldown_cmark::{CodeBlockKind, Event, Tag, TagEnd};
#[cfg(any(feature = "mermaid", feature = "latex"))]
use std::cell::RefCell;
#[cfg(any(feature = "mermaid", feature = "latex"))]
use std::rc::Rc;
use syntect::highlighting::ThemeSet;
use syntect::html::css_for_theme_with_class_style;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
#[cfg(feature = "latex")]
const LATEX_PARSE_ERROR_MARKER: &str = "[PARSE ERROR:";
#[cfg(feature = "mermaid")]
fn mermaid_error_html(msg: &str, code: &str) -> String {
format!(
"<div class=\"mermaid-error\"><p><em>Mermaid render error: {}</em></p>\
<pre><code class=\"language-mermaid\">{}</code></pre></div>",
html_escape::encode_text(msg),
html_escape::encode_text(code),
)
}
#[cfg(feature = "latex")]
fn math_error_html(msg: &str, code: &str) -> String {
format!(
"<div class=\"math-error\"><p><em>LaTeX render error: {}</em></p>\
<pre><code class=\"language-math\">{}</code></pre></div>",
html_escape::encode_text(msg),
html_escape::encode_text(code),
)
}
#[cfg(feature = "latex")]
fn math_error_html_inline(msg: &str, code: &str) -> String {
format!(
"<span class=\"math-error-inline\"><em>LaTeX error: {}</em> \
<code class=\"language-math\">{}</code></span>",
html_escape::encode_text(msg),
html_escape::encode_text(code),
)
}
#[cfg(feature = "latex")]
fn parse_error_message(mathml: &str, code: &str) -> String {
let marker = mathml
.find(LATEX_PARSE_ERROR_MARKER)
.map(|start| {
let rest = &mathml[start..];
let end = rest.find(']').map_or(rest.len(), |i| i + 1);
&rest[..end]
})
.unwrap_or("[PARSE ERROR]");
format!("{marker} in formula: {code}")
}
pub(crate) fn get_highlighting_css(theme_name: &str) -> String {
let ts = ThemeSet::load_defaults();
ts.themes
.get(theme_name)
.and_then(|theme| {
css_for_theme_with_class_style(theme, syntect::html::ClassStyle::Spaced).ok()
})
.unwrap_or_default()
}
#[derive(Debug, Default)]
pub struct SyntaxPreprocessor<'a, I: Iterator<Item = Event<'a>>> {
parent: I,
#[cfg(any(feature = "mermaid", feature = "latex"))]
error_policy: EmbeddedContentErrorPolicy,
#[cfg(any(feature = "mermaid", feature = "latex"))]
error_sink: Rc<RefCell<Option<NoteError>>>,
}
impl<'a, I: Iterator<Item = Event<'a>>> SyntaxPreprocessor<'a, I> {
#[cfg_attr(
not(any(feature = "mermaid", feature = "latex")),
allow(unused_variables)
)]
pub fn new(parent: I, error_policy: EmbeddedContentErrorPolicy) -> Self {
Self {
parent,
#[cfg(any(feature = "mermaid", feature = "latex"))]
error_policy,
#[cfg(any(feature = "mermaid", feature = "latex"))]
error_sink: Rc::new(RefCell::new(None)),
}
}
#[cfg(any(feature = "mermaid", feature = "latex"))]
pub(crate) fn error_sink(&self) -> Rc<RefCell<Option<NoteError>>> {
self.error_sink.clone()
}
#[cfg(any(feature = "mermaid", feature = "latex"))]
fn embedded_error_event(
&self,
renderer: &str,
inline_html: String,
msg: String,
) -> Event<'static> {
match self.error_policy {
EmbeddedContentErrorPolicy::HardError => {
let mut sink = self.error_sink.borrow_mut();
if sink.is_none() {
*sink = Some(NoteError::RenderError {
renderer: renderer.to_string(),
msg,
});
}
Event::Html(String::new().into())
}
EmbeddedContentErrorPolicy::Inline => {
log::warn!("{renderer} failed to render: {msg}");
Event::Html(inline_html.into())
}
}
}
#[cfg(feature = "latex")]
fn render_math(&self, latex: &str, style: latex2mathml::DisplayStyle) -> Event<'static> {
let inline = matches!(style, latex2mathml::DisplayStyle::Inline);
match latex2mathml::latex_to_mathml(latex, style) {
Ok(mathml) if mathml.contains(LATEX_PARSE_ERROR_MARKER) => {
let msg = parse_error_message(&mathml, latex);
let tagged = mathml.replace(
&format!("<mtext>{LATEX_PARSE_ERROR_MARKER}"),
&format!("<mtext class=\"math-parse-error\">{LATEX_PARSE_ERROR_MARKER}"),
);
self.embedded_error_event("LaTeX", tagged, msg)
}
Ok(mathml) => Event::Html(mathml.into()),
Err(e) => {
let msg = e.to_string();
let html = if inline {
math_error_html_inline(&msg, latex)
} else {
math_error_html(&msg, latex)
};
self.embedded_error_event("LaTeX", html, msg)
}
}
}
}
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SyntaxPreprocessor<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
let lang = match self.parent.next()? {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) if !lang.is_empty() => lang,
#[cfg(feature = "latex")]
Event::Code(c) if c.len() > 1 && c.starts_with('$') && c.ends_with('$') => {
return Some(self.render_math(&c[1..c.len() - 1], latex2mathml::DisplayStyle::Inline));
}
#[cfg(feature = "latex")]
Event::InlineMath(c) => {
return Some(self.render_math(c.as_ref(), latex2mathml::DisplayStyle::Inline));
}
#[cfg(feature = "latex")]
Event::DisplayMath(c) => {
return Some(self.render_math(c.as_ref(), latex2mathml::DisplayStyle::Block));
}
other => return Some(other),
};
let mut code = String::new();
let mut event = self.parent.next();
while let Some(Event::Text(ref code_block)) = event {
code.push_str(code_block);
event = self.parent.next();
}
debug_assert!(matches!(event, Some(Event::End(TagEnd::CodeBlock))));
#[cfg(feature = "latex")]
if lang.as_ref() == "math" {
return Some(self.render_math(&code, latex2mathml::DisplayStyle::Block));
}
#[cfg(feature = "mermaid")]
if lang.as_ref() == "mermaid" {
const NO_AUTO_WRAP_LABEL_CHARS: usize = 100_000;
let render = || {
let mut options = mermaid_rs_renderer::RenderOptions::default();
options.layout.max_label_width_chars = NO_AUTO_WRAP_LABEL_CHARS;
mermaid_rs_renderer::render_with_options(&code, options)
};
let result = match std::panic::catch_unwind(render) {
Ok(r) => r.map_err(|e| e.to_string()),
Err(_) => Err("internal renderer panic".to_string()),
};
return Some(match result {
Ok(svg) => Event::Html(format!("<div class=\"mermaid\">{svg}</div>").into()),
Err(msg) => self.embedded_error_event("Mermaid", mermaid_error_html(&msg, &code), msg),
});
}
let mut html = String::with_capacity(code.len() + code.len() * 3 / 2 + 20);
let ss = SyntaxSet::load_defaults_newlines();
let sr = match ss.find_syntax_by_token(lang.as_ref()) {
Some(sr) => {
html.push_str("<pre><code class=\"language-");
html.push_str(lang.as_ref());
html.push_str("\">");
sr
}
None => {
log::debug!(
"renderer: no syntax definition found for: `{}`",
lang.as_ref()
);
html.push_str("<pre><code>");
ss.find_syntax_plain_text()
}
};
let mut html_generator =
ClassedHTMLGenerator::new_with_class_style(sr, &ss, ClassStyle::Spaced);
for line in LinesWithEndings::from(&code) {
html_generator
.parse_html_for_line_which_includes_newline(line)
.unwrap_or_default();
}
html.push_str(html_generator.finalize().as_str());
html.push_str("</code></pre>");
Some(Event::Html(html.into()))
}
}
#[cfg(test)]
mod test {
#[cfg(feature = "mermaid")]
use crate::config::EmbeddedContentErrorPolicy;
use crate::highlight::SyntaxPreprocessor;
use pulldown_cmark::{Options, Parser, html};
#[cfg(feature = "latex")]
#[test]
fn test_latex_math() {
let input: &str = "casual $\\sum_{n=0}^\\infty \\frac{1}{n!}$ text";
let expected = "<p>casual <math xmlns=";
let options = Options::all();
let parser = Parser::new_ext(input, options);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
println!("Rendered: {}", rendered);
assert!(rendered.starts_with(expected));
let input: &str = "casual `$\\sum_{n=0}^\\infty \\frac{1}{n!}$` text";
let expected = "<p>casual <math xmlns=";
let options = Options::all();
let parser = Parser::new_ext(input, options);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert!(rendered.starts_with(expected));
let input = "text\n$$\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
\\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n$$";
let expected = "<p>text\n\
<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
<mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
<mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
<mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
<msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
<mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
<mi>Z</mi></math></p>\n";
let options = Options::all();
let parser = Parser::new_ext(input, options);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert_eq!(rendered, expected);
let input = "text\n```math\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
\\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n```";
let expected = "<p>text</p>\n\
<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
<mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
<mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
<mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
<msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
<mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
<mi>Z</mi></math>";
let options = Options::all();
let parser = Parser::new_ext(input, options);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert_eq!(rendered, expected);
}
#[test]
fn test_rust_source() {
let input: &str = "```rust\n\
fn main() {\n\
println!(\"Hello, world!\");\n\
}\n\
```";
let expected = "<pre><code class=\"language-rust\">\
<span class=\"source rust\">";
let parser = Parser::new(input);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert!(rendered.starts_with(expected));
}
#[test]
fn test_plain_text() {
let input: &str = "```\nSome\nText\n```";
let expected = "<pre><code>\
Some\nText\n</code></pre>\n";
let parser = Parser::new(input);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert_eq!(rendered, expected);
}
#[test]
fn test_unkown_source() {
let input: &str = "```abc\n\
fn main() {\n\
println!(\"Hello, world!\");\n\
}\n\
```";
let expected = "<pre><code>\
<span class=\"text plain\">fn main()";
let parser = Parser::new(input);
let processed = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, processed);
assert!(rendered.starts_with(expected));
}
#[test]
fn test_md() {
let markdown_input = "# Titel\n\nBody";
let expected = "<h1>Titel</h1>\n<p>Body</p>\n";
let options = Options::all();
let parser = Parser::new_ext(markdown_input, options);
let parser = SyntaxPreprocessor::new(parser, Default::default());
let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
html::push_html(&mut html_output, parser);
assert_eq!(html_output, expected);
}
#[test]
fn test_indented() {
let markdown_input = r#"
1. test
```bash
wget getreu.net
echo test
```
"#;
let expected = "<ol>\n<li>\n<p>test</p>\n<pre>\
<code class=\"language-bash\">\
<span class=\"source shell bash\">\
<span class=\"meta function-call shell\">\
<span class=\"variable function shell\">wget</span></span>";
let options = Options::all();
let parser = Parser::new_ext(markdown_input, options);
let parser = SyntaxPreprocessor::new(parser, Default::default());
let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
html::push_html(&mut html_output, parser);
assert!(html_output.starts_with(expected));
}
#[cfg(feature = "mermaid")]
#[test]
fn test_mermaid_diagram() {
let input = "```mermaid\ngraph TD\n A --> B\n```";
let parser = Parser::new_ext(input, Options::all());
let parser = SyntaxPreprocessor::new(parser, Default::default());
let mut rendered = String::new();
html::push_html(&mut rendered, parser);
assert!(rendered.contains("<div class=\"mermaid\">"));
assert!(rendered.contains("<svg"));
}
#[cfg(feature = "mermaid")]
#[test]
fn test_mermaid_invalid_inline() {
let input = "```mermaid\nthis is not a valid mermaid diagram !!!\n```";
let parser = Parser::new_ext(input, Options::all());
let parser = SyntaxPreprocessor::new(parser, EmbeddedContentErrorPolicy::Inline);
let mut rendered = String::new();
html::push_html(&mut rendered, parser);
assert!(rendered.contains("mermaid-error"));
}
}