1use core::fmt;
2
3use crate::alloc::fmt::TryWrite;
4use crate::alloc::{self, try_vec, HashMap, String, Vec};
5use crate::doc::TestParams;
6
7use anyhow::Result;
8use pulldown_cmark::{Alignment, CodeBlockKind, CowStr, Event, LinkType, Tag, TagEnd};
9use pulldown_cmark_escape::{escape_href, escape_html, StrWrite};
10use syntect::html::{ClassStyle, ClassedHTMLGenerator};
11use syntect::parsing::{SyntaxReference, SyntaxSet};
12
13pub(crate) const RUST_TOKEN: &str = "rust";
14pub(crate) const RUNE_TOKEN: &str = "rune";
15
16use Event::*;
17
18enum TableState {
19 Head,
20 Body,
21}
22
23struct StringWriter<'a> {
24 string: &'a mut String,
25}
26
27impl StrWrite for StringWriter<'_> {
28 type Error = alloc::Error;
29
30 fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
31 self.string.try_push_str(s)
32 }
33
34 fn write_fmt(&mut self, args: fmt::Arguments) -> Result<(), Self::Error> {
35 TryWrite::write_fmt(self.string, args)
36 }
37}
38
39struct Writer<'a, 'o, I> {
40 syntax_set: Option<&'a SyntaxSet>,
41 iter: I,
42 out: StringWriter<'o>,
43 tests: Option<&'o mut Vec<(String, TestParams)>>,
44 codeblock: Option<(
45 Option<(&'a SyntaxSet, &'a SyntaxReference)>,
46 Option<TestParams>,
47 )>,
48 table_state: TableState,
49 table_alignments: Vec<Alignment>,
50 table_cell_index: usize,
51 numbers: HashMap<CowStr<'a>, usize>,
52}
53
54impl<'a, I> Writer<'a, '_, I>
55where
56 I: Iterator<Item = Event<'a>>,
57{
58 #[inline]
59 fn write(&mut self, s: &str) -> Result<()> {
60 self.out.string.try_push_str(s)?;
61 Ok(())
62 }
63
64 fn run(mut self) -> Result<()> {
65 while let Some(event) = self.iter.next() {
66 match event {
67 Start(tag) => {
68 self.start_tag(tag)?;
69 }
70 End(tag) => {
71 self.end_tag(tag)?;
72 }
73 Text(text) => {
74 if let Some((syntax, params)) = self.codeblock {
75 let mut string = String::new();
76
77 let s = (self.tests.is_some() && params.is_some()).then_some(&mut string);
78
79 let html = match syntax {
80 Some((syntax_set, syntax)) => {
81 render_code_by_syntax(syntax_set, syntax, text.lines(), s)?
82 }
83 None => render_code_without_syntax(text.lines(), s)?,
84 };
85
86 if let Some(params) = params {
87 if let Some(tests) = self.tests.as_mut() {
88 tests.try_push((string, params))?;
89 }
90 }
91
92 self.write(&html)?;
93 } else {
94 escape_html(&mut self.out, &text)?;
95 }
96 }
97 Code(text) => {
98 self.write("<code>")?;
99 escape_html(&mut self.out, &text)?;
100 self.write("</code>")?;
101 }
102 Html(html) => {
103 self.write(&html)?;
104 }
105 SoftBreak => {
106 self.write(" ")?;
107 }
108 HardBreak => {
109 self.write("<br />")?;
110 }
111 Rule => {
112 self.write("<hr />")?;
113 }
114 FootnoteReference(name) => {
115 let len = self.numbers.len() + 1;
116 self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
117 escape_html(&mut self.out, &name)?;
118 self.write("\">")?;
119 let number = *self.numbers.entry(name).or_try_insert(len)?;
120 write!(&mut self.out, "{}", number)?;
121 self.write("</a></sup>")?;
122 }
123 TaskListMarker(true) => {
124 self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>")?;
125 }
126 TaskListMarker(false) => {
127 self.write("<input disabled=\"\" type=\"checkbox\"/>")?;
128 }
129 InlineMath(text) => {
130 self.write(r#"<span class="math math-inline">"#)?;
131 escape_html(&mut self.out, &text)?;
132 self.write("</span>")?;
133 }
134 DisplayMath(text) => {
135 self.write(r#"<span class="math math-display">"#)?;
136 escape_html(&mut self.out, &text)?;
137 self.write("</span>")?;
138 }
139 InlineHtml(text) => {
140 self.write(&text)?;
141 }
142 }
143 }
144
145 Ok(())
146 }
147
148 fn start_tag(&mut self, tag: Tag<'a>) -> Result<()> {
149 match tag {
150 Tag::Paragraph => {
151 self.write("<p>")?;
152 }
153 Tag::Heading {
154 level, id, classes, ..
155 } => {
156 self.write("<")?;
157
158 write!(&mut self.out, "{}", level)?;
159
160 if let Some(id) = id {
161 self.write(" id=\"")?;
162 escape_html(&mut self.out, &id)?;
163 self.write("\"")?;
164 }
165
166 let mut classes = classes.iter();
167
168 if let Some(class) = classes.next() {
169 self.write(" class=\"")?;
170 escape_html(&mut self.out, class)?;
171 for class in classes {
172 self.write(" ")?;
173 escape_html(&mut self.out, class)?;
174 }
175 self.write("\"")?;
176 }
177
178 self.write(">")?;
179 }
180 Tag::Table(alignments) => {
181 self.table_alignments = alignments.try_into()?;
182 self.write("<table>")?;
183 }
184 Tag::TableHead => {
185 self.table_state = TableState::Head;
186 self.table_cell_index = 0;
187 self.write("<thead><tr>")?;
188 }
189 Tag::TableRow => {
190 self.table_cell_index = 0;
191 self.write("<tr>")?;
192 }
193 Tag::TableCell => {
194 match self.table_state {
195 TableState::Head => {
196 self.write("<th")?;
197 }
198 TableState::Body => {
199 self.write("<td")?;
200 }
201 }
202
203 match self.table_alignments.get(self.table_cell_index) {
204 Some(Alignment::Left) => {
205 self.write(" style=\"text-align: left\">")?;
206 }
207 Some(Alignment::Center) => {
208 self.write(" style=\"text-align: center\">")?;
209 }
210 Some(Alignment::Right) => {
211 self.write(" style=\"text-align: right\">")?;
212 }
213 _ => {
214 self.write(">")?;
215 }
216 }
217 }
218 Tag::BlockQuote(..) => {
219 self.write("<blockquote>")?;
220 }
221 Tag::CodeBlock(kind) => {
222 self.write("<pre><code class=\"language-")?;
223 let (lang, syntax, params) = self.find_syntax(&kind);
224 self.codeblock = Some((syntax, params));
225 escape_href(&mut self.out, lang)?;
226 self.write("\">")?;
227 }
228 Tag::List(Some(1)) => {
229 self.write("<ol>")?;
230 }
231 Tag::List(Some(start)) => {
232 self.write("<ol start=\"")?;
233 write!(&mut self.out, "{}", start)?;
234 self.write("\">")?;
235 }
236 Tag::List(None) => {
237 self.write("<ul>")?;
238 }
239 Tag::Item => {
240 self.write("<li>")?;
241 }
242 Tag::Emphasis => {
243 self.write("<em>")?;
244 }
245 Tag::Strong => {
246 self.write("<strong>")?;
247 }
248 Tag::Strikethrough => {
249 self.write("<del>")?;
250 }
251 Tag::Link {
252 link_type: LinkType::Email,
253 dest_url,
254 title,
255 ..
256 } => {
257 self.write("<a href=\"mailto:")?;
258 escape_href(&mut self.out, &dest_url)?;
259 if !title.is_empty() {
260 self.write("\" title=\"")?;
261 escape_html(&mut self.out, &title)?;
262 }
263 self.write("\">")?;
264 }
265 Tag::Link {
266 dest_url, title, ..
267 } => {
268 self.write("<a href=\"")?;
269 escape_href(&mut self.out, &dest_url)?;
270 if !title.is_empty() {
271 self.write("\" title=\"")?;
272 escape_html(&mut self.out, &title)?;
273 }
274 self.write("\">")?;
275 }
276 Tag::Image {
277 dest_url, title, ..
278 } => {
279 self.write("<img src=\"")?;
280 escape_href(&mut self.out, &dest_url)?;
281 self.write("\" alt=\"")?;
282 self.raw_text()?;
283
284 if !title.is_empty() {
285 self.write("\" title=\"")?;
286 escape_html(&mut self.out, &title)?;
287 }
288
289 self.write("\" />")?;
290 }
291 Tag::FootnoteDefinition(name) => {
292 self.write("<div class=\"footnote-definition\" id=\"")?;
293 escape_html(&mut self.out, &name).map_err(|_| fmt::Error)?;
294 self.write("\"><sup class=\"footnote-definition-label\">")?;
295 let len = self.numbers.len() + 1;
296 let number = *self.numbers.entry(name).or_try_insert(len)?;
297 write!(&mut self.out, "{}", number)?;
298 self.write("</sup>")?;
299 }
300 Tag::HtmlBlock => {}
301 Tag::DefinitionList => {
302 self.write("<dl>")?;
303 }
304 Tag::DefinitionListTitle => {
305 self.write("<dt>")?;
306 }
307 Tag::DefinitionListDefinition => {
308 self.write("<dd>")?;
309 }
310 Tag::MetadataBlock(..) => {}
311 }
312
313 Ok(())
314 }
315
316 fn find_syntax<'input>(
317 &mut self,
318 kind: &'input CodeBlockKind<'input>,
319 ) -> (
320 &'input str,
321 Option<(&'a SyntaxSet, &'a SyntaxReference)>,
322 Option<TestParams>,
323 ) {
324 let mut syntax = None;
325 let mut params = TestParams::default();
326
327 if let CodeBlockKind::Fenced(fences) = &kind {
328 for token in fences.split(',') {
329 let (token, lookup, is_rune) = match token.trim() {
330 "no_run" => {
331 params.no_run = true;
332 continue;
333 }
334 "should_panic" => {
335 params.should_panic = true;
336 continue;
337 }
338 "ignore" => {
339 params.ignore = true;
340 continue;
341 }
342 RUNE_TOKEN => (RUNE_TOKEN, RUST_TOKEN, true),
343 token => (token, token, false),
344 };
345
346 if syntax.is_none() {
347 match self.syntax_set {
348 Some(syntax_set) => {
349 if let Some(s) = syntax_set.find_syntax_by_token(lookup) {
350 syntax = Some((token, Some((syntax_set, s)), is_rune));
351 }
352 }
353 None => {
354 syntax = Some((token, None, is_rune));
355 }
356 }
357 }
358 }
359 }
360
361 if let Some((token, syntax, is_rune)) = syntax {
362 return (token, syntax, is_rune.then_some(params));
363 }
364
365 if let Some(syntax_set) = self.syntax_set {
366 let Some(syntax) = syntax_set.find_syntax_by_token(RUST_TOKEN) else {
367 return (
368 "text",
369 Some((syntax_set, syntax_set.find_syntax_plain_text())),
370 Some(params),
371 );
372 };
373
374 (RUNE_TOKEN, Some((syntax_set, syntax)), Some(params))
375 } else {
376 (RUNE_TOKEN, None, Some(params))
377 }
378 }
379
380 fn end_tag(&mut self, tag: TagEnd) -> Result<()> {
381 match tag {
382 TagEnd::Paragraph => {
383 self.write("</p>")?;
384 }
385 TagEnd::Heading(level) => {
386 self.write("</")?;
387 write!(&mut self.out, "{}", level)?;
388 self.write(">")?;
389 }
390 TagEnd::Table => {
391 self.write("</tbody></table>")?;
392 }
393 TagEnd::TableHead => {
394 self.write("</tr></thead><tbody>")?;
395 self.table_state = TableState::Body;
396 }
397 TagEnd::TableRow => {
398 self.write("</tr>")?;
399 }
400 TagEnd::TableCell => {
401 match self.table_state {
402 TableState::Head => {
403 self.write("</th>")?;
404 }
405 TableState::Body => {
406 self.write("</td>")?;
407 }
408 }
409 self.table_cell_index += 1;
410 }
411 TagEnd::BlockQuote(_) => {
412 self.write("</blockquote>")?;
413 }
414 TagEnd::CodeBlock => {
415 self.write("</code></pre>")?;
416 self.codeblock = None;
417 }
418 TagEnd::List(true) => {
419 self.write("</ol>")?;
420 }
421 TagEnd::List(false) => {
422 self.write("</ul>")?;
423 }
424 TagEnd::Item => {
425 self.write("</li>")?;
426 }
427 TagEnd::Emphasis => {
428 self.write("</em>")?;
429 }
430 TagEnd::Strong => {
431 self.write("</strong>")?;
432 }
433 TagEnd::Strikethrough => {
434 self.write("</del>")?;
435 }
436 TagEnd::Link => {
437 self.write("</a>")?;
438 }
439 TagEnd::Image => (),
440 TagEnd::FootnoteDefinition => {
441 self.write("</div>")?;
442 }
443 TagEnd::HtmlBlock => {}
444 TagEnd::DefinitionList => {
445 self.write("</dl>")?;
446 }
447 TagEnd::DefinitionListTitle => {
448 self.write("</dt>")?;
449 }
450 TagEnd::DefinitionListDefinition => {
451 self.write("</dd>")?;
452 }
453 TagEnd::MetadataBlock(..) => {}
454 }
455
456 Ok(())
457 }
458
459 fn raw_text(&mut self) -> Result<()> {
460 let mut nest = 0;
461
462 while let Some(event) = self.iter.next() {
463 match event {
464 Start(_) => nest += 1,
465 End(_) => {
466 if nest == 0 {
467 break;
468 }
469 nest -= 1;
470 }
471 Html(text) | Code(text) | Text(text) | InlineMath(text) | DisplayMath(text)
472 | InlineHtml(text) => {
473 escape_html(&mut self.out, &text).map_err(|_| fmt::Error)?;
474 }
475 SoftBreak | HardBreak | Rule => {
476 self.write(" ")?;
477 }
478 FootnoteReference(name) => {
479 let len = self.numbers.len() + 1;
480 let number = *self.numbers.entry(name).or_try_insert(len)?;
481 write!(self.out, "[{}]", number)?;
482 }
483 TaskListMarker(true) => self.write("[x]")?,
484 TaskListMarker(false) => self.write("[ ]")?,
485 }
486 }
487
488 Ok(())
489 }
490}
491
492pub(crate) fn push_html<'a, I>(
494 syntax_set: Option<&'a SyntaxSet>,
495 string: &'a mut String,
496 iter: I,
497 tests: Option<&'a mut Vec<(String, TestParams)>>,
498) -> Result<()>
499where
500 I: Iterator<Item = Event<'a>>,
501{
502 let writer = Writer {
503 syntax_set,
504 iter,
505 out: StringWriter { string },
506 tests,
507 codeblock: None,
508 table_state: TableState::Head,
509 table_alignments: try_vec![],
510 table_cell_index: 0,
511 numbers: HashMap::new(),
512 };
513
514 writer.run()?;
515 Ok(())
516}
517
518pub(super) fn render_code_by_syntax<I>(
520 syntax_set: &SyntaxSet,
521 syntax: &SyntaxReference,
522 lines: I,
523 mut out: Option<&mut String>,
524) -> Result<String>
525where
526 I: IntoIterator,
527 I::Item: AsRef<str>,
528{
529 let mut buf = String::new();
530 let mut gen =
531 ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, ClassStyle::Spaced);
532
533 for line in lines {
534 let line = line.as_ref();
535 let line = line.strip_prefix(' ').unwrap_or(line);
536
537 if line.starts_with('#') {
538 if let Some(o) = out.as_mut() {
539 o.try_push_str(line.trim_start_matches('#'))?;
540 o.try_push('\n')?;
541 }
542
543 continue;
544 }
545
546 if let Some(o) = out.as_mut() {
547 o.try_push_str(line)?;
548 o.try_push('\n')?;
549 }
550
551 buf.clear();
552 buf.try_push_str(line)?;
553 buf.try_push('\n')?;
554 gen.parse_html_for_line_which_includes_newline(&buf)?;
555 }
556
557 Ok(gen.finalize().try_into()?)
558}
559
560pub(super) fn render_code_without_syntax<I>(
561 lines: I,
562 mut out: Option<&mut String>,
563) -> Result<String>
564where
565 I: IntoIterator,
566 I::Item: AsRef<str>,
567{
568 let mut buf = String::new();
569
570 for line in lines {
571 let line = line.as_ref();
572 let line = line.strip_prefix(' ').unwrap_or(line);
573
574 if line.starts_with('#') {
575 if let Some(o) = out.as_mut() {
576 o.try_push_str(line.trim_start_matches('#'))?;
577 o.try_push('\n')?;
578 }
579
580 continue;
581 }
582
583 if let Some(o) = out.as_mut() {
584 o.try_push_str(line)?;
585 o.try_push('\n')?;
586 }
587
588 buf.try_push_str(line)?;
589 buf.try_push('\n')?;
590 }
591
592 Ok(buf)
593}