rune/doc/
markdown.rs

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
492/// Process markdown html and captures tests.
493pub(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
518/// Render documentation.
519pub(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}