1use 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
10pub(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#[derive(Debug, Default)]
24pub struct SyntaxPreprocessor<'a, I: Iterator<Item = Event<'a>>> {
25 parent: I,
26}
27
28impl<'a, I: Iterator<Item = Event<'a>>> SyntaxPreprocessor<'a, I> {
30 pub fn new(parent: I) -> Self {
31 Self { parent }
32 }
33}
34
35impl<'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 let lang = match self.parent.next()? {
42 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) if !lang.is_empty() => lang,
43 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 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 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 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 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 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 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 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}