tpnote_lib/
highlight.rs

1//! Syntax highlighting for (inline) source code blocks in Markdown input.
2
3use pulldown_cmark::{CodeBlockKind, Event, Tag, TagEnd};
4use syntect::highlighting::ThemeSet;
5use syntect::html::css_for_theme_with_class_style;
6use syntect::html::{ClassStyle, ClassedHTMLGenerator};
7use syntect::parsing::SyntaxSet;
8use syntect::util::LinesWithEndings;
9
10/// Get the viewer syntax highlighting CSS configuration.
11pub(crate) fn get_highlighting_css(theme_name: &str) -> String {
12    let ts = ThemeSet::load_defaults();
13
14    ts.themes
15        .get(theme_name)
16        .and_then(|theme| {
17            css_for_theme_with_class_style(theme, syntect::html::ClassStyle::Spaced).ok()
18        })
19        .unwrap_or_default()
20}
21
22/// A wrapper for a `pulldown_cmark` event iterator.
23#[derive(Debug, Default)]
24pub struct SyntaxPreprocessor<'a, I: Iterator<Item = Event<'a>>> {
25    parent: I,
26}
27
28/// Constructor.
29impl<'a, I: Iterator<Item = Event<'a>>> SyntaxPreprocessor<'a, I> {
30    pub fn new(parent: I) -> Self {
31        Self { parent }
32    }
33}
34
35/// Implement `Iterator` for wrapper `SyntaxPreprocessor`.
36impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SyntaxPreprocessor<'a, I> {
37    type Item = Event<'a>;
38
39    fn next(&mut self) -> Option<Self::Item> {
40        // Detect inline LaTeX.
41        let lang = match self.parent.next()? {
42            Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) if !lang.is_empty() => lang,
43            // This is the depreciated inline math syntax.
44            // It is kept here for backwards compatibility.
45            Event::Code(c) if c.len() > 1 && c.starts_with('$') && c.ends_with('$') => {
46                return Some(Event::Html(
47                    latex2mathml::latex_to_mathml(
48                        &c[1..c.len() - 1],
49                        latex2mathml::DisplayStyle::Inline,
50                    )
51                    .unwrap_or_else(|e| e.to_string())
52                    .into(),
53                ));
54            }
55            Event::InlineMath(c) => {
56                return Some(Event::Html(
57                    latex2mathml::latex_to_mathml(c.as_ref(), latex2mathml::DisplayStyle::Inline)
58                        .unwrap_or_else(|e| e.to_string())
59                        .into(),
60                ));
61            }
62            Event::DisplayMath(c) => {
63                return Some(Event::Html(
64                    latex2mathml::latex_to_mathml(c.as_ref(), latex2mathml::DisplayStyle::Block)
65                        .unwrap_or_else(|e| e.to_string())
66                        .into(),
67                ));
68            }
69            other => return Some(other),
70        };
71
72        let mut code = String::new();
73        let mut event = self.parent.next();
74        while let Some(Event::Text(ref code_block)) = event {
75            code.push_str(code_block);
76            event = self.parent.next();
77        }
78
79        debug_assert!(matches!(event, Some(Event::End(TagEnd::CodeBlock))));
80
81        if lang.as_ref() == "math" {
82            return Some(Event::Html(
83                latex2mathml::latex_to_mathml(&code, latex2mathml::DisplayStyle::Block)
84                    .unwrap_or_else(|e| e.to_string())
85                    .into(),
86            ));
87        }
88
89        let mut html = String::with_capacity(code.len() + code.len() * 3 / 2 + 20);
90
91        // Use default syntax styling.
92        let ss = SyntaxSet::load_defaults_newlines();
93        let sr = match ss.find_syntax_by_token(lang.as_ref()) {
94            Some(sr) => {
95                html.push_str("<pre><code class=\"language-");
96                html.push_str(lang.as_ref());
97                html.push_str("\">");
98                sr
99            }
100            None => {
101                log::debug!(
102                    "renderer: no syntax definition found for: `{}`",
103                    lang.as_ref()
104                );
105                html.push_str("<pre><code>");
106                ss.find_syntax_plain_text()
107            }
108        };
109        let mut html_generator =
110            ClassedHTMLGenerator::new_with_class_style(sr, &ss, ClassStyle::Spaced);
111        for line in LinesWithEndings::from(&code) {
112            html_generator
113                .parse_html_for_line_which_includes_newline(line)
114                .unwrap_or_default();
115        }
116        html.push_str(html_generator.finalize().as_str());
117
118        html.push_str("</code></pre>");
119
120        Some(Event::Html(html.into()))
121    }
122}
123
124#[cfg(test)]
125mod test {
126    use crate::highlight::SyntaxPreprocessor;
127    use pulldown_cmark::{html, Options, Parser};
128
129    #[test]
130    fn test_latex_math() {
131        // Inline math.
132        let input: &str = "casual $\\sum_{n=0}^\\infty \\frac{1}{n!}$ text";
133
134        let expected = "<p>casual <math xmlns=";
135
136        let options = Options::all();
137        let parser = Parser::new_ext(input, options);
138        let processed = SyntaxPreprocessor::new(parser);
139
140        let mut rendered = String::new();
141        html::push_html(&mut rendered, processed);
142        println!("Rendered: {}", rendered);
143        assert!(rendered.starts_with(expected));
144
145        //
146        // Depreciated inline math.
147        // This code might be removed later.
148        let input: &str = "casual `$\\sum_{n=0}^\\infty \\frac{1}{n!}$` text";
149
150        let expected = "<p>casual <math xmlns=";
151
152        let options = Options::all();
153        let parser = Parser::new_ext(input, options);
154        let processed = SyntaxPreprocessor::new(parser);
155
156        let mut rendered = String::new();
157        html::push_html(&mut rendered, processed);
158        assert!(rendered.starts_with(expected));
159
160        //
161        // Block math 1
162        let input = "text\n$$\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
163            \\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n$$";
164
165        let expected = "<p>text\n\
166            <math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
167            <mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
168            <mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
169            <mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
170            <msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
171            <mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
172            <mi>Z</mi></math></p>\n";
173
174        let options = Options::all();
175        let parser = Parser::new_ext(input, options);
176        let processed = SyntaxPreprocessor::new(parser);
177
178        let mut rendered = String::new();
179        html::push_html(&mut rendered, processed);
180        assert_eq!(rendered, expected);
181
182        // Block math 2
183        let input = "text\n```math\nR(X, Y)Z = \\nabla_X\\nabla_Y Z - \
184            \\nabla_Y \\nabla_X Z - \\nabla_{[X, Y]} Z\n```";
185
186        let expected = "<p>text</p>\n\
187            <math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">\
188            <mi>R</mi><mo>(</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>)</mo>\
189            <mi>Z</mi><mo>=</mo><msub><mo>∇</mo><mi>X</mi></msub><msub><mo>∇</mo>\
190            <mi>Y</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo><mi>Y</mi></msub>\
191            <msub><mo>∇</mo><mi>X</mi></msub><mi>Z</mi><mo>-</mo><msub><mo>∇</mo>\
192            <mrow><mo>[</mo><mi>X</mi><mo>,</mo><mi>Y</mi><mo>]</mo></mrow></msub>\
193            <mi>Z</mi></math>";
194
195        let options = Options::all();
196        let parser = Parser::new_ext(input, options);
197        let processed = SyntaxPreprocessor::new(parser);
198
199        let mut rendered = String::new();
200        html::push_html(&mut rendered, processed);
201        assert_eq!(rendered, expected);
202    }
203
204    #[test]
205    fn test_rust_source() {
206        let input: &str = "```rust\n\
207            fn main() {\n\
208                println!(\"Hello, world!\");\n\
209            }\n\
210            ```";
211
212        let expected = "<pre><code class=\"language-rust\">\
213            <span class=\"source rust\">";
214
215        let parser = Parser::new(input);
216        let processed = SyntaxPreprocessor::new(parser);
217
218        let mut rendered = String::new();
219        html::push_html(&mut rendered, processed);
220        assert!(rendered.starts_with(expected));
221    }
222
223    #[test]
224    fn test_plain_text() {
225        let input: &str = "```\nSome\nText\n```";
226
227        let expected = "<pre><code>\
228            Some\nText\n</code></pre>\n";
229
230        let parser = Parser::new(input);
231        let processed = SyntaxPreprocessor::new(parser);
232
233        let mut rendered = String::new();
234        html::push_html(&mut rendered, processed);
235        assert_eq!(rendered, expected);
236    }
237
238    #[test]
239    fn test_unkown_source() {
240        let input: &str = "```abc\n\
241            fn main() {\n\
242                println!(\"Hello, world!\");\n\
243            }\n\
244            ```";
245
246        let expected = "<pre><code>\
247            <span class=\"text plain\">fn main()";
248
249        let parser = Parser::new(input);
250        let processed = SyntaxPreprocessor::new(parser);
251
252        let mut rendered = String::new();
253        html::push_html(&mut rendered, processed);
254        assert!(rendered.starts_with(expected));
255    }
256
257    #[test]
258    fn test_md() {
259        let markdown_input = "# Titel\n\nBody";
260        let expected = "<h1>Titel</h1>\n<p>Body</p>\n";
261
262        let options = Options::all();
263        let parser = Parser::new_ext(markdown_input, options);
264        let parser = SyntaxPreprocessor::new(parser);
265
266        // Write to String buffer.
267        let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
268        html::push_html(&mut html_output, parser);
269        assert_eq!(html_output, expected);
270    }
271
272    #[test]
273    fn test_indented() {
274        let markdown_input = r#"
2751. test
276
277   ```bash
278   wget getreu.net
279   echo test
280   ```
281"#;
282
283        let expected = "<ol>\n<li>\n<p>test</p>\n<pre>\
284            <code class=\"language-bash\">\
285            <span class=\"source shell bash\">\
286            <span class=\"meta function-call shell\">\
287            <span class=\"variable function shell\">wget</span></span>";
288        let options = Options::all();
289        let parser = Parser::new_ext(markdown_input, options);
290        let parser = SyntaxPreprocessor::new(parser);
291
292        // Write to String buffer.
293        let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
294        html::push_html(&mut html_output, parser);
295        assert!(html_output.starts_with(expected));
296    }
297}