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
6use std::iter;
7
8pub 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
52pub 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
86fn split_paragraphs(lines: &[String]) -> Vec<String> {
87 let mut last_line: usize = 0;
88 iterimpl Iterator::from_fn(|| {
89 let slice: &[String] = &lines[last_line..];
90 let start: usize = slice.iter().position(|s| !is_blank(s)).unwrap_or(default:0);
91
92 let slice: &[String] = &slice[start..];
93 let len: usize = slice
94 .iter()
95 .position(|s| is_blank(s))
96 .unwrap_or(default: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
109fn remove_period(mut s: String) -> String {
110 if s.ends_with('.') && !s.ends_with("..") {
111 s.pop();
112 }
113 s
114}
115
116fn is_blank(s: &str) -> bool {
117 s.trim().is_empty()
118}
119
120fn 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(sep:" ")
126}
127