1use crate::config::EmbeddedContentErrorPolicy;
4#[cfg(any(feature = "mermaid", feature = "latex"))]
5use crate::error::NoteError;
6use pulldown_cmark::{CodeBlockKind, Event, Tag, TagEnd};
7#[cfg(any(feature = "mermaid", feature = "latex"))]
8use std::cell::RefCell;
9#[cfg(any(feature = "mermaid", feature = "latex"))]
10use std::rc::Rc;
11use syntect::highlighting::ThemeSet;
12use syntect::html::css_for_theme_with_class_style;
13use syntect::html::{ClassStyle, ClassedHTMLGenerator};
14use syntect::parsing::SyntaxSet;
15use syntect::util::LinesWithEndings;
16
17#[cfg(feature = "latex")]
22const LATEX_PARSE_ERROR_MARKER: &str = "[PARSE ERROR:";
23
24#[cfg(feature = "mermaid")]
29fn mermaid_error_html(msg: &str, code: &str) -> String {
30 format!(
31 "<div class=\"mermaid-error\"><p><em>Mermaid render error: {}</em></p>\
32 <pre><code class=\"language-mermaid\">{}</code></pre></div>",
33 html_escape::encode_text(msg),
34 html_escape::encode_text(code),
35 )
36}
37
38#[cfg(feature = "latex")]
42fn math_error_html(msg: &str, code: &str) -> String {
43 format!(
44 "<div class=\"math-error\"><p><em>LaTeX render error: {}</em></p>\
45 <pre><code class=\"language-math\">{}</code></pre></div>",
46 html_escape::encode_text(msg),
47 html_escape::encode_text(code),
48 )
49}
50
51#[cfg(feature = "latex")]
56fn math_error_html_inline(msg: &str, code: &str) -> String {
57 format!(
58 "<span class=\"math-error-inline\"><em>LaTeX error: {}</em> \
59 <code class=\"language-math\">{}</code></span>",
60 html_escape::encode_text(msg),
61 html_escape::encode_text(code),
62 )
63}
64
65#[cfg(feature = "latex")]
69fn parse_error_message(mathml: &str, code: &str) -> String {
70 let marker = mathml
71 .find(LATEX_PARSE_ERROR_MARKER)
72 .map(|start| {
73 let rest = &mathml[start..];
74 let end = rest.find(']').map_or(rest.len(), |i| i + 1);
75 &rest[..end]
76 })
77 .unwrap_or("[PARSE ERROR]");
78 format!("{marker} in formula: {code}")
79}
80
81pub(crate) fn get_highlighting_css(theme_name: &str) -> String {
83 let ts = ThemeSet::load_defaults();
84
85 ts.themes
86 .get(theme_name)
87 .and_then(|theme| {
88 css_for_theme_with_class_style(theme, syntect::html::ClassStyle::Spaced).ok()
89 })
90 .unwrap_or_default()
91}
92
93#[derive(Debug, Default)]
95pub struct SyntaxPreprocessor<'a, I: Iterator<Item = Event<'a>>> {
96 parent: I,
97 #[cfg(any(feature = "mermaid", feature = "latex"))]
99 error_policy: EmbeddedContentErrorPolicy,
100 #[cfg(any(feature = "mermaid", feature = "latex"))]
105 error_sink: Rc<RefCell<Option<NoteError>>>,
106}
107
108impl<'a, I: Iterator<Item = Event<'a>>> SyntaxPreprocessor<'a, I> {
110 #[cfg_attr(
111 not(any(feature = "mermaid", feature = "latex")),
112 allow(unused_variables)
113 )]
114 pub fn new(parent: I, error_policy: EmbeddedContentErrorPolicy) -> Self {
115 Self {
116 parent,
117 #[cfg(any(feature = "mermaid", feature = "latex"))]
118 error_policy,
119 #[cfg(any(feature = "mermaid", feature = "latex"))]
120 error_sink: Rc::new(RefCell::new(None)),
121 }
122 }
123
124 #[cfg(any(feature = "mermaid", feature = "latex"))]
127 pub(crate) fn error_sink(&self) -> Rc<RefCell<Option<NoteError>>> {
128 self.error_sink.clone()
129 }
130
131 #[cfg(any(feature = "mermaid", feature = "latex"))]
138 fn embedded_error_event(
139 &self,
140 renderer: &str,
141 inline_html: String,
142 msg: String,
143 ) -> Event<'static> {
144 match self.error_policy {
145 EmbeddedContentErrorPolicy::HardError => {
146 let mut sink = self.error_sink.borrow_mut();
148 if sink.is_none() {
149 *sink = Some(NoteError::RenderError {
150 renderer: renderer.to_string(),
151 msg,
152 });
153 }
154 Event::Html(String::new().into())
156 }
157 EmbeddedContentErrorPolicy::Inline => {
158 log::warn!("{renderer} failed to render: {msg}");
159 Event::Html(inline_html.into())
160 }
161 }
162 }
163
164 #[cfg(feature = "latex")]
168 fn render_math(&self, latex: &str, style: latex2mathml::DisplayStyle) -> Event<'static> {
169 let inline = matches!(style, latex2mathml::DisplayStyle::Inline);
170 match latex2mathml::latex_to_mathml(latex, style) {
171 Ok(mathml) if mathml.contains(LATEX_PARSE_ERROR_MARKER) => {
180 let msg = parse_error_message(&mathml, latex);
181 let tagged = mathml.replace(
185 &format!("<mtext>{LATEX_PARSE_ERROR_MARKER}"),
186 &format!("<mtext class=\"math-parse-error\">{LATEX_PARSE_ERROR_MARKER}"),
187 );
188 self.embedded_error_event("LaTeX", tagged, msg)
189 }
190 Ok(mathml) => Event::Html(mathml.into()),
191 Err(e) => {
192 let msg = e.to_string();
193 let html = if inline {
194 math_error_html_inline(&msg, latex)
195 } else {
196 math_error_html(&msg, latex)
197 };
198 self.embedded_error_event("LaTeX", html, msg)
199 }
200 }
201 }
202}
203
204impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SyntaxPreprocessor<'a, I> {
206 type Item = Event<'a>;
207
208 fn next(&mut self) -> Option<Self::Item> {
209 let lang = match self.parent.next()? {
211 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) if !lang.is_empty() => lang,
212 #[cfg(feature = "latex")]
215 Event::Code(c) if c.len() > 1 && c.starts_with('$') && c.ends_with('$') => {
216 return Some(self.render_math(&c[1..c.len() - 1], latex2mathml::DisplayStyle::Inline));
217 }
218 #[cfg(feature = "latex")]
219 Event::InlineMath(c) => {
220 return Some(self.render_math(c.as_ref(), latex2mathml::DisplayStyle::Inline));
221 }
222 #[cfg(feature = "latex")]
223 Event::DisplayMath(c) => {
224 return Some(self.render_math(c.as_ref(), latex2mathml::DisplayStyle::Block));
225 }
226 other => return Some(other),
227 };
228
229 let mut code = String::new();
230 let mut event = self.parent.next();
231 while let Some(Event::Text(ref code_block)) = event {
232 code.push_str(code_block);
233 event = self.parent.next();
234 }
235
236 debug_assert!(matches!(event, Some(Event::End(TagEnd::CodeBlock))));
237
238 #[cfg(feature = "latex")]
239 if lang.as_ref() == "math" {
240 return Some(self.render_math(&code, latex2mathml::DisplayStyle::Block));
241 }
242
243 #[cfg(feature = "mermaid")]
244 if lang.as_ref() == "mermaid" {
245 const NO_AUTO_WRAP_LABEL_CHARS: usize = 100_000;
260 let render = || {
261 let mut options = mermaid_rs_renderer::RenderOptions::default();
262 options.layout.max_label_width_chars = NO_AUTO_WRAP_LABEL_CHARS;
263 mermaid_rs_renderer::render_with_options(&code, options)
264 };
265 let result = match std::panic::catch_unwind(render) {
266 Ok(r) => r.map_err(|e| e.to_string()),
267 Err(_) => Err("internal renderer panic".to_string()),
268 };
269 return Some(match result {
270 Ok(svg) => Event::Html(format!("<div class=\"mermaid\">{svg}</div>").into()),
275 Err(msg) => self.embedded_error_event("Mermaid", mermaid_error_html(&msg, &code), msg),
276 });
277 }
278
279 let mut html = String::with_capacity(code.len() + code.len() * 3 / 2 + 20);
280
281 let ss = SyntaxSet::load_defaults_newlines();
283 let sr = match ss.find_syntax_by_token(lang.as_ref()) {
284 Some(sr) => {
285 html.push_str("<pre><code class=\"language-");
286 html.push_str(lang.as_ref());
287 html.push_str("\">");
288 sr
289 }
290 None => {
291 log::debug!(
292 "renderer: no syntax definition found for: `{}`",
293 lang.as_ref()
294 );
295 html.push_str("<pre><code>");
296 ss.find_syntax_plain_text()
297 }
298 };
299 let mut html_generator =
300 ClassedHTMLGenerator::new_with_class_style(sr, &ss, ClassStyle::Spaced);
301 for line in LinesWithEndings::from(&code) {
302 html_generator
303 .parse_html_for_line_which_includes_newline(line)
304 .unwrap_or_default();
305 }
306 html.push_str(html_generator.finalize().as_str());
307
308 html.push_str("</code></pre>");
309
310 Some(Event::Html(html.into()))
311 }
312}
313
314#[cfg(test)]
315mod test {
316 #[cfg(feature = "mermaid")]
317 use crate::config::EmbeddedContentErrorPolicy;
318 use crate::highlight::SyntaxPreprocessor;
319 use pulldown_cmark::{Options, Parser, html};
320
321 #[cfg(feature = "latex")]
322 #[test]
323 fn test_latex_math() {
324 let input: &str = "casual $\\sum_{n=0}^\\infty \\frac{1}{n!}$ text";
326
327 let expected = "<p>casual <math xmlns=";
328
329 let options = Options::all();
330 let parser = Parser::new_ext(input, options);
331 let processed = SyntaxPreprocessor::new(parser, Default::default());
332
333 let mut rendered = String::new();
334 html::push_html(&mut rendered, processed);
335 println!("Rendered: {}", rendered);
336 assert!(rendered.starts_with(expected));
337
338 let input: &str = "casual `$\\sum_{n=0}^\\infty \\frac{1}{n!}$` text";
342
343 let expected = "<p>casual <math xmlns=";
344
345 let options = Options::all();
346 let parser = Parser::new_ext(input, options);
347 let processed = SyntaxPreprocessor::new(parser, Default::default());
348
349 let mut rendered = String::new();
350 html::push_html(&mut rendered, processed);
351 assert!(rendered.starts_with(expected));
352
353 let input = "text\n$$\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
356 \\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n$$";
357
358 let expected = "<p>text\n\
359 <math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
360 <mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
361 <mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
362 <mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
363 <msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
364 <mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
365 <mi>Z</mi></math></p>\n";
366
367 let options = Options::all();
368 let parser = Parser::new_ext(input, options);
369 let processed = SyntaxPreprocessor::new(parser, Default::default());
370
371 let mut rendered = String::new();
372 html::push_html(&mut rendered, processed);
373 assert_eq!(rendered, expected);
374
375 let input = "text\n```math\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
377 \\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n```";
378
379 let expected = "<p>text</p>\n\
380 <math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
381 <mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
382 <mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
383 <mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
384 <msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
385 <mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
386 <mi>Z</mi></math>";
387
388 let options = Options::all();
389 let parser = Parser::new_ext(input, options);
390 let processed = SyntaxPreprocessor::new(parser, Default::default());
391
392 let mut rendered = String::new();
393 html::push_html(&mut rendered, processed);
394 assert_eq!(rendered, expected);
395 }
396
397 #[test]
398 fn test_rust_source() {
399 let input: &str = "```rust\n\
400 fn main() {\n\
401 println!(\"Hello, world!\");\n\
402 }\n\
403 ```";
404
405 let expected = "<pre><code class=\"language-rust\">\
406 <span class=\"source rust\">";
407
408 let parser = Parser::new(input);
409 let processed = SyntaxPreprocessor::new(parser, Default::default());
410
411 let mut rendered = String::new();
412 html::push_html(&mut rendered, processed);
413 assert!(rendered.starts_with(expected));
414 }
415
416 #[test]
417 fn test_plain_text() {
418 let input: &str = "```\nSome\nText\n```";
419
420 let expected = "<pre><code>\
421 Some\nText\n</code></pre>\n";
422
423 let parser = Parser::new(input);
424 let processed = SyntaxPreprocessor::new(parser, Default::default());
425
426 let mut rendered = String::new();
427 html::push_html(&mut rendered, processed);
428 assert_eq!(rendered, expected);
429 }
430
431 #[test]
432 fn test_unkown_source() {
433 let input: &str = "```abc\n\
434 fn main() {\n\
435 println!(\"Hello, world!\");\n\
436 }\n\
437 ```";
438
439 let expected = "<pre><code>\
440 <span class=\"text plain\">fn main()";
441
442 let parser = Parser::new(input);
443 let processed = SyntaxPreprocessor::new(parser, Default::default());
444
445 let mut rendered = String::new();
446 html::push_html(&mut rendered, processed);
447 assert!(rendered.starts_with(expected));
448 }
449
450 #[test]
451 fn test_md() {
452 let markdown_input = "# Titel\n\nBody";
453 let expected = "<h1>Titel</h1>\n<p>Body</p>\n";
454
455 let options = Options::all();
456 let parser = Parser::new_ext(markdown_input, options);
457 let parser = SyntaxPreprocessor::new(parser, Default::default());
458
459 let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
461 html::push_html(&mut html_output, parser);
462 assert_eq!(html_output, expected);
463 }
464
465 #[test]
466 fn test_indented() {
467 let markdown_input = r#"
4681. test
469
470 ```bash
471 wget getreu.net
472 echo test
473 ```
474"#;
475
476 let expected = "<ol>\n<li>\n<p>test</p>\n<pre>\
477 <code class=\"language-bash\">\
478 <span class=\"source shell bash\">\
479 <span class=\"meta function-call shell\">\
480 <span class=\"variable function shell\">wget</span></span>";
481 let options = Options::all();
482 let parser = Parser::new_ext(markdown_input, options);
483 let parser = SyntaxPreprocessor::new(parser, Default::default());
484
485 let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
487 html::push_html(&mut html_output, parser);
488 assert!(html_output.starts_with(expected));
489 }
490
491 #[cfg(feature = "mermaid")]
492 #[test]
493 fn test_mermaid_diagram() {
494 let input = "```mermaid\ngraph TD\n A --> B\n```";
495
496 let parser = Parser::new_ext(input, Options::all());
497 let parser = SyntaxPreprocessor::new(parser, Default::default());
498
499 let mut rendered = String::new();
500 html::push_html(&mut rendered, parser);
501 assert!(rendered.contains("<div class=\"mermaid\">"));
502 assert!(rendered.contains("<svg"));
503 }
504
505 #[cfg(feature = "mermaid")]
506 #[test]
507 fn test_mermaid_invalid_inline() {
508 let input = "```mermaid\nthis is not a valid mermaid diagram !!!\n```";
511
512 let parser = Parser::new_ext(input, Options::all());
513 let parser = SyntaxPreprocessor::new(parser, EmbeddedContentErrorPolicy::Inline);
514
515 let mut rendered = String::new();
516 html::push_html(&mut rendered, parser);
517 assert!(rendered.contains("mermaid-error"));
518 }
519}