sync_readme/content/rustdoc/
code_block.rs

1use std::borrow::Cow;
2
3use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd};
4
5pub(super) fn convert<'a, 'b>(events: impl IntoIterator<Item = Event<'a>> + 'b) -> impl Iterator<Item = Event<'a>> + 'b {
6    let mut in_codeblock = None;
7    events.into_iter().map(move |mut event| {
8        if let Some(is_rust) = in_codeblock {
9            match &mut event {
10                Event::Text(text) => {
11                    if !text.ends_with('\n') {
12                        // workaround for https://github.com/Byron/pulldown-cmark-to-cmark/issues/48
13                        *text = format!("{text}\n").into();
14                    }
15                    if is_rust {
16                        // Hide lines starting with any number of whitespace
17                        // followed by `# ` (comments), or just `#`. But `## `
18                        // should be converted to `# `.
19                        *text = text
20                            .lines()
21                            .filter_map(|line| {
22                                // Adapted from
23                                // https://github.com/rust-lang/rust/blob/942db6782f4a28c55b0b75b38fd4394d0483390f/src/librustdoc/html/markdown.rs#L169-L182.
24                                let trimmed = line.trim();
25                                if trimmed.starts_with("##") {
26                                    // It would be nice to reuse
27                                    // `pulldown_cmark::CowStr` here, but (at
28                                    // least as of version 0.12.2) it doesn't
29                                    // support collecting into a `String`.
30                                    Some(Cow::Owned(line.replacen("##", "#", 1)))
31                                } else if trimmed.starts_with("# ") {
32                                    // Hidden line.
33                                    None
34                                } else if trimmed == "#" {
35                                    // A plain # is a hidden line.
36                                    None
37                                } else {
38                                    Some(Cow::Borrowed(line))
39                                }
40                            })
41                            .flat_map(|line| [line, Cow::Borrowed("\n")])
42                            .collect::<String>()
43                            .into();
44                    }
45                }
46                Event::End(TagEnd::CodeBlock) => {}
47                _ => unreachable!(),
48            }
49        }
50
51        match &mut event {
52            Event::Start(Tag::CodeBlock(kind)) => {
53                let is_rust;
54                match kind {
55                    CodeBlockKind::Indented => {
56                        is_rust = true;
57                        *kind = CodeBlockKind::Fenced("rust".into());
58                    }
59                    CodeBlockKind::Fenced(tag) => {
60                        is_rust = update_codeblock_tag(tag);
61                    }
62                }
63
64                assert!(in_codeblock.is_none());
65                in_codeblock = Some(is_rust);
66            }
67            Event::End(TagEnd::CodeBlock) => {
68                assert!(in_codeblock.is_some());
69                in_codeblock = None;
70            }
71            _ => {}
72        }
73        event
74    })
75}
76fn is_attribute_tag(tag: &str) -> bool {
77    // https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#attributes
78    // to support future rust edition, `edition\d{4}` treated as attribute tag
79    matches!(tag, "" | "ignore" | "should_panic" | "no_run" | "compile_fail")
80        || tag
81            .strip_prefix("edition")
82            .map(|x| x.len() == 4 && x.chars().all(|ch| ch.is_ascii_digit()))
83            .unwrap_or_default()
84}
85
86fn update_codeblock_tag(tag: &mut CowStr<'_>) -> bool {
87    let mut tag_count = 0;
88    let is_rust = tag.split(',').filter(|tag| !is_attribute_tag(tag)).all(|tag| {
89        tag_count += 1;
90        tag == "rust"
91    });
92    if is_rust && tag_count == 0 {
93        if tag.is_empty() {
94            *tag = "rust".into();
95        } else {
96            *tag = format!("rust,{tag}").into();
97        }
98    }
99    is_rust
100}