1 //! The preprocessing we apply to doc comments.
2 //!
3 //! #[derive(Parser)] works in terms of "paragraphs". Paragraph is a sequence of
4 //! non-empty adjacent lines, delimited by sequences of blank (whitespace only) lines.
5 
6 use std::iter;
7 
extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String>8 pub fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String> {
9     // multiline comments (`/** ... */`) may have LFs (`\n`) in them,
10     // we need to split so we could handle the lines correctly
11     //
12     // we also need to remove leading and trailing blank lines
13     let mut lines: Vec<_> = attrs
14         .iter()
15         .filter(|attr| attr.path().is_ident("doc"))
16         .filter_map(|attr| {
17             // non #[doc = "..."] attributes are not our concern
18             // we leave them for rustc to handle
19             match &attr.meta {
20                 syn::Meta::NameValue(syn::MetaNameValue {
21                     value:
22                         syn::Expr::Lit(syn::ExprLit {
23                             lit: syn::Lit::Str(s),
24                             ..
25                         }),
26                     ..
27                 }) => Some(s.value()),
28                 _ => None,
29             }
30         })
31         .skip_while(|s| is_blank(s))
32         .flat_map(|s| {
33             let lines = s
34                 .split('\n')
35                 .map(|s| {
36                     // remove one leading space no matter what
37                     let s = s.strip_prefix(' ').unwrap_or(s);
38                     s.to_owned()
39                 })
40                 .collect::<Vec<_>>();
41             lines
42         })
43         .collect();
44 
45     while let Some(true) = lines.last().map(|s| is_blank(s)) {
46         lines.pop();
47     }
48 
49     lines
50 }
51 
format_doc_comment( lines: &[String], preprocess: bool, force_long: bool, ) -> (Option<String>, Option<String>)52 pub fn format_doc_comment(
53     lines: &[String],
54     preprocess: bool,
55     force_long: bool,
56 ) -> (Option<String>, Option<String>) {
57     if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) {
58         let (short, long) = if preprocess {
59             let paragraphs = split_paragraphs(lines);
60             let short = paragraphs[0].clone();
61             let long = paragraphs.join("\n\n");
62             (remove_period(short), long)
63         } else {
64             let short = lines[..first_blank].join("\n");
65             let long = lines.join("\n");
66             (short, long)
67         };
68 
69         (Some(short), Some(long))
70     } else {
71         let (short, long) = if preprocess {
72             let short = merge_lines(lines);
73             let long = force_long.then(|| short.clone());
74             let short = remove_period(short);
75             (short, long)
76         } else {
77             let short = lines.join("\n");
78             let long = force_long.then(|| short.clone());
79             (short, long)
80         };
81 
82         (Some(short), long)
83     }
84 }
85 
split_paragraphs(lines: &[String]) -> Vec<String>86 fn split_paragraphs(lines: &[String]) -> Vec<String> {
87     let mut last_line = 0;
88     iter::from_fn(|| {
89         let slice = &lines[last_line..];
90         let start = slice.iter().position(|s| !is_blank(s)).unwrap_or(0);
91 
92         let slice = &slice[start..];
93         let len = slice
94             .iter()
95             .position(|s| is_blank(s))
96             .unwrap_or(slice.len());
97 
98         last_line += start + len;
99 
100         if len != 0 {
101             Some(merge_lines(&slice[..len]))
102         } else {
103             None
104         }
105     })
106     .collect()
107 }
108 
remove_period(mut s: String) -> String109 fn remove_period(mut s: String) -> String {
110     if s.ends_with('.') && !s.ends_with("..") {
111         s.pop();
112     }
113     s
114 }
115 
is_blank(s: &str) -> bool116 fn is_blank(s: &str) -> bool {
117     s.trim().is_empty()
118 }
119 
merge_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String120 fn merge_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String {
121     lines
122         .into_iter()
123         .map(|s| s.as_ref().trim().to_owned())
124         .collect::<Vec<_>>()
125         .join(" ")
126 }
127