clap_derive/utils/
doc_comments.rs
1#[cfg(feature = "unstable-markdown")]
7use markdown::parse_markdown;
8
9pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String> {
10 let mut lines: Vec<_> = attrs
15 .iter()
16 .filter(|attr| attr.path().is_ident("doc"))
17 .filter_map(|attr| {
18 match &attr.meta {
21 syn::Meta::NameValue(syn::MetaNameValue {
22 value:
23 syn::Expr::Lit(syn::ExprLit {
24 lit: syn::Lit::Str(s),
25 ..
26 }),
27 ..
28 }) => Some(s.value()),
29 _ => None,
30 }
31 })
32 .skip_while(|s| is_blank(s))
33 .flat_map(|s| {
34 let lines = s
35 .split('\n')
36 .map(|s| {
37 let s = s.strip_prefix(' ').unwrap_or(s);
39 s.to_owned()
40 })
41 .collect::<Vec<_>>();
42 lines
43 })
44 .collect();
45
46 while let Some(true) = lines.last().map(|s| is_blank(s)) {
47 lines.pop();
48 }
49
50 lines
51}
52
53pub(crate) fn format_doc_comment(
54 lines: &[String],
55 preprocess: bool,
56 force_long: bool,
57) -> (Option<String>, Option<String>) {
58 if preprocess {
59 let (short, long) = parse_markdown(lines);
60 let long = long.or_else(|| force_long.then(|| short.clone()));
61
62 (Some(remove_period(short)), long)
63 } else if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) {
64 let short = lines[..first_blank].join("\n");
65 let long = lines.join("\n");
66
67 (Some(short), Some(long))
68 } else {
69 let short = lines.join("\n");
70 let long = force_long.then(|| short.clone());
71
72 (Some(short), long)
73 }
74}
75
76#[cfg(not(feature = "unstable-markdown"))]
77fn split_paragraphs(lines: &[String]) -> Vec<String> {
78 use std::iter;
79
80 let mut last_line = 0;
81 iter::from_fn(|| {
82 let slice = &lines[last_line..];
83 let start = slice.iter().position(|s| !is_blank(s)).unwrap_or(0);
84
85 let slice = &slice[start..];
86 let len = slice
87 .iter()
88 .position(|s| is_blank(s))
89 .unwrap_or(slice.len());
90
91 last_line += start + len;
92
93 if len != 0 {
94 Some(merge_lines(&slice[..len]))
95 } else {
96 None
97 }
98 })
99 .collect()
100}
101
102fn remove_period(mut s: String) -> String {
103 if s.ends_with('.') && !s.ends_with("..") {
104 s.pop();
105 }
106 s
107}
108
109fn is_blank(s: &str) -> bool {
110 s.trim().is_empty()
111}
112
113#[cfg(not(feature = "unstable-markdown"))]
114fn merge_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String {
115 lines
116 .into_iter()
117 .map(|s| s.as_ref().trim().to_owned())
118 .collect::<Vec<_>>()
119 .join(" ")
120}
121
122#[cfg(not(feature = "unstable-markdown"))]
123fn parse_markdown(lines: &[String]) -> (String, Option<String>) {
124 if lines.iter().any(|s| is_blank(s)) {
125 let paragraphs = split_paragraphs(lines);
126 let short = paragraphs[0].clone();
127 let long = paragraphs.join("\n\n");
128 (short, Some(long))
129 } else {
130 let short = merge_lines(lines);
131 (short, None)
132 }
133}
134
135#[cfg(feature = "unstable-markdown")]
136mod markdown {
137 use anstyle::{Reset, Style};
138 use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
139 use std::fmt;
140 use std::fmt::Write;
141 use std::ops::AddAssign;
142
143 #[derive(Default)]
144 struct MarkdownWriter {
145 output: String,
146 prefix: String,
148 hanging_paragraph: bool,
150 dirty_line: bool,
152 styles: Vec<Style>,
153 }
154
155 impl MarkdownWriter {
156 fn newline(&mut self) {
157 self.reset();
158 self.output.push('\n');
159 self.dirty_line = false;
160 }
161 fn endline(&mut self) {
162 if self.dirty_line {
163 self.newline();
164 }
165 }
166 fn new_paragraph(&mut self) {
167 self.endline();
168 self.hanging_paragraph = true;
169 }
170
171 fn write_fmt(&mut self, arguments: fmt::Arguments<'_>) {
172 if self.hanging_paragraph {
173 self.hanging_paragraph = false;
174 self.newline();
175 }
176 if !self.dirty_line {
177 self.output.push_str(&self.prefix);
178 self.apply_styles();
179 self.dirty_line = true;
180 }
181 self.output.write_fmt(arguments).unwrap();
182 }
183
184 fn start_link(&mut self, dest_url: pulldown_cmark::CowStr<'_>) {
185 write!(self, "\x1B]8;;{dest_url}\x1B\\");
186 }
187 fn end_link(&mut self) {
188 write!(self, "\x1B]8;;\x1B\\");
189 }
190
191 fn start_style(&mut self, style: Style) {
192 self.styles.push(style);
193 write!(self, "{style}");
194 }
195 fn end_style(&mut self, style: Style) {
196 let last_style = self.styles.pop();
197 debug_assert_eq!(last_style.unwrap(), style);
198
199 write!(self, "{Reset}");
200 self.apply_styles();
201 }
202
203 fn reset(&mut self) {
204 write!(self, "{Reset}");
205 }
206
207 fn apply_styles(&mut self) {
208 for style in &self.styles {
213 write!(self.output, "{style}").unwrap();
214 }
215 }
216
217 fn remove_prefix(&mut self, quote_prefix: &str) {
218 debug_assert!(self.prefix.ends_with(quote_prefix));
219 let new_len = self.prefix.len() - quote_prefix.len();
220 self.prefix.truncate(new_len);
221 }
222
223 fn add_prefix(&mut self, quote_prefix: &str) {
224 if self.hanging_paragraph {
225 self.hanging_paragraph = false;
226 self.newline();
227 }
228 self.prefix += quote_prefix;
229 }
230 }
231
232 pub(super) fn parse_markdown(input: &[String]) -> (String, Option<String>) {
233 let parsing_options = Options::ENABLE_STRIKETHROUGH;
235 let style_heading = Style::new().bold().underline();
237 let style_emphasis = Style::new().italic();
238 let style_strong = Style::new().bold();
239 let style_strike_through = Style::new().strikethrough();
240 let style_link = Style::new().underline();
241 let style_code = Style::new().bold();
242 let list_symbol = '-';
243 let quote_prefix = "| ";
244 let indentation = " ";
245
246 let input = input.join("\n");
247 let input = Parser::new_ext(&input, parsing_options);
248
249 let mut short = None;
250 let mut has_details = false;
251
252 let mut writer = MarkdownWriter::default();
253
254 let mut list_indices = Vec::new();
255
256 for event in input {
257 if short.is_some() {
258 has_details = true;
259 }
260 match event {
261 Event::Start(Tag::Paragraph) => { }
262 Event::End(TagEnd::Paragraph) => {
263 if short.is_none() {
264 short = Some(writer.output.trim().to_owned());
265 }
266 writer.new_paragraph();
267 }
268
269 Event::Start(Tag::Heading { .. }) => writer.start_style(style_heading),
270 Event::End(TagEnd::Heading(..)) => {
271 writer.end_style(style_heading);
272 writer.new_paragraph();
273 }
274
275 Event::Start(Tag::Image { .. } | Tag::HtmlBlock) => { }
276 Event::End(TagEnd::Image) => { }
277 Event::End(TagEnd::HtmlBlock) => writer.new_paragraph(),
278
279 Event::Start(Tag::BlockQuote(_)) => writer.add_prefix(quote_prefix),
280 Event::End(TagEnd::BlockQuote(_)) => {
281 writer.remove_prefix(quote_prefix);
282 writer.new_paragraph();
283 }
284
285 Event::Start(Tag::CodeBlock(_)) => {
286 writer.add_prefix(indentation);
287 writer.start_style(style_code);
288 }
289 Event::End(TagEnd::CodeBlock) => {
290 writer.remove_prefix(indentation);
291 writer.end_style(style_code);
292 writer.dirty_line = false;
293 writer.hanging_paragraph = true;
294 }
295
296 Event::Start(Tag::List(list_start)) => {
297 list_indices.push(list_start);
298 writer.endline();
299 }
300 Event::End(TagEnd::List(_)) => {
301 let list = list_indices.pop();
302 debug_assert!(list.is_some());
303 if list_indices.is_empty() {
304 writer.new_paragraph();
305 }
306 }
307 Event::Start(Tag::Item) => {
308 if let Some(Some(index)) = list_indices.last_mut() {
309 write!(writer, "{index}. ");
310 index.add_assign(1);
311 } else {
312 write!(writer, "{list_symbol} ");
313 }
314 writer.add_prefix(indentation);
315 }
316 Event::End(TagEnd::Item) => {
317 writer.remove_prefix(indentation);
318 writer.endline();
319 }
320
321 Event::Start(Tag::Emphasis) => writer.start_style(style_emphasis),
322 Event::End(TagEnd::Emphasis) => writer.end_style(style_emphasis),
323 Event::Start(Tag::Strong) => writer.start_style(style_strong),
324 Event::End(TagEnd::Strong) => writer.end_style(style_strong),
325 Event::Start(Tag::Strikethrough) => writer.start_style(style_strike_through),
326 Event::End(TagEnd::Strikethrough) => writer.end_style(style_strike_through),
327
328 Event::Start(Tag::Link { dest_url, .. }) => {
329 writer.start_link(dest_url);
330 writer.start_style(style_link);
331 }
332 Event::End(TagEnd::Link) => {
333 writer.end_link();
334 writer.end_style(style_link);
335 }
336
337 Event::Text(segment) => {
338 let mut lines = segment.lines();
340 write!(writer, "{}", lines.next().unwrap());
342 for line in lines {
343 writer.endline();
344 write!(writer, "{line}");
345 }
346 if segment.ends_with('\n') {
347 writer.endline();
348 }
349 }
350
351 Event::Code(code) => {
352 writer.start_style(style_code);
353 write!(writer, "{code}");
354 writer.end_style(style_code);
355 }
356
357 Event::Html(html) => write!(writer, "{html}"),
359 Event::InlineHtml(html) => write!(writer, "{html}"),
361 Event::SoftBreak => write!(writer, " "),
362 Event::HardBreak => writer.endline(),
363
364 Event::Rule => {
365 writer.new_paragraph();
366 write!(writer, "---");
367 writer.new_paragraph();
368 }
369
370 Event::Start(
372 Tag::FootnoteDefinition(_)
373 | Tag::DefinitionList
374 | Tag::DefinitionListTitle
375 | Tag::DefinitionListDefinition
376 | Tag::Table(_)
377 | Tag::TableHead
378 | Tag::TableRow
379 | Tag::TableCell
380 | Tag::MetadataBlock(_)
381 | Tag::Superscript
382 | Tag::Subscript,
383 )
384 | Event::End(
385 TagEnd::FootnoteDefinition
386 | TagEnd::DefinitionList
387 | TagEnd::DefinitionListTitle
388 | TagEnd::DefinitionListDefinition
389 | TagEnd::Table
390 | TagEnd::TableHead
391 | TagEnd::TableRow
392 | TagEnd::TableCell
393 | TagEnd::MetadataBlock(_)
394 | TagEnd::Superscript
395 | TagEnd::Subscript,
396 )
397 | Event::InlineMath(_)
398 | Event::DisplayMath(_)
399 | Event::FootnoteReference(_)
400 | Event::TaskListMarker(_) => {
401 unimplemented!("feature not enabled {event:?}")
402 }
403 }
404 }
405 let short = short.unwrap_or_else(|| writer.output.trim_end().to_owned());
406 let long = writer.output.trim_end();
407 let long = has_details.then(|| long.to_owned());
408 (short, long)
409 }
410}