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}