]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/unindent_comments.rs
Rollup merge of #93461 - dtolnay:fmtyield, r=davidtwco
[rust.git] / src / librustdoc / passes / unindent_comments.rs
1 //! Removes excess indentation on comments in order for the Markdown
2 //! to be parsed correctly. This is necessary because the convention for
3 //! writing documentation is to provide a space between the /// or //! marker
4 //! and the doc text, but Markdown is whitespace-sensitive. For example,
5 //! a block of text with four-space indentation is parsed as a code block,
6 //! so if we didn't unindent comments, these list items
7 //!
8 //! /// A list:
9 //! ///
10 //! ///    - Foo
11 //! ///    - Bar
12 //!
13 //! would be parsed as if they were in a code block, which is likely not what the user intended.
14 use std::cmp;
15
16 use rustc_span::symbol::kw;
17
18 use crate::clean::{self, DocFragment, DocFragmentKind, Item};
19 use crate::core::DocContext;
20 use crate::fold::{self, DocFolder};
21 use crate::passes::Pass;
22
23 #[cfg(test)]
24 mod tests;
25
26 crate const UNINDENT_COMMENTS: Pass = Pass {
27     name: "unindent-comments",
28     run: unindent_comments,
29     description: "removes excess indentation on comments in order for markdown to like it",
30 };
31
32 crate fn unindent_comments(krate: clean::Crate, _: &mut DocContext<'_>) -> clean::Crate {
33     CommentCleaner.fold_crate(krate)
34 }
35
36 struct CommentCleaner;
37
38 impl fold::DocFolder for CommentCleaner {
39     fn fold_item(&mut self, mut i: Item) -> Option<Item> {
40         i.attrs.unindent_doc_comments();
41         Some(self.fold_item_recur(i))
42     }
43 }
44
45 impl clean::Attributes {
46     crate fn unindent_doc_comments(&mut self) {
47         unindent_fragments(&mut self.doc_strings);
48     }
49 }
50
51 fn unindent_fragments(docs: &mut Vec<DocFragment>) {
52     // `add` is used in case the most common sugared doc syntax is used ("/// "). The other
53     // fragments kind's lines are never starting with a whitespace unless they are using some
54     // markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
55     // we need to take into account the fact that the minimum indent minus one (to take this
56     // whitespace into account).
57     //
58     // For example:
59     //
60     // /// hello!
61     // #[doc = "another"]
62     //
63     // In this case, you want "hello! another" and not "hello!  another".
64     let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
65         && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
66     {
67         // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
68         // "decide" how much the minimum indent will be.
69         1
70     } else {
71         0
72     };
73
74     // `min_indent` is used to know how much whitespaces from the start of each lines must be
75     // removed. Example:
76     //
77     // ///     hello!
78     // #[doc = "another"]
79     //
80     // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
81     // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
82     // (5 - 1) whitespaces.
83     let min_indent = match docs
84         .iter()
85         .map(|fragment| {
86             fragment.doc.as_str().lines().fold(usize::MAX, |min_indent, line| {
87                 if line.chars().all(|c| c.is_whitespace()) {
88                     min_indent
89                 } else {
90                     // Compare against either space or tab, ignoring whether they are
91                     // mixed or not.
92                     let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
93                     cmp::min(min_indent, whitespace)
94                         + if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
95                 }
96             })
97         })
98         .min()
99     {
100         Some(x) => x,
101         None => return,
102     };
103
104     for fragment in docs {
105         if fragment.doc == kw::Empty {
106             continue;
107         }
108
109         let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
110             min_indent - add
111         } else {
112             min_indent
113         };
114
115         fragment.indent = min_indent;
116     }
117 }