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 | #[cfg (feature = "unstable-markdown" )] |
7 | use markdown::parse_markdown; |
8 | |
9 | pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String> { |
10 | // multiline comments (`/** ... */`) may have LFs (`\n`) in them, |
11 | // we need to split so we could handle the lines correctly |
12 | // |
13 | // we also need to remove leading and trailing blank lines |
14 | let mut lines: Vec<_> = attrs |
15 | .iter() |
16 | .filter(|attr| attr.path().is_ident("doc" )) |
17 | .filter_map(|attr| { |
18 | // non #[doc = "..."] attributes are not our concern |
19 | // we leave them for rustc to handle |
20 | 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 | // remove one leading space no matter what |
38 | 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 | |
53 | pub(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: String, long: Option) = parse_markdown(lines); |
60 | let long: Option = long.or_else(|| force_long.then(|| short.clone())); |
61 | |
62 | (Some(remove_period(short)), long) |
63 | } else if let Some(first_blank: usize) = lines.iter().position(|s: &String| is_blank(s)) { |
64 | let short: String = lines[..first_blank].join(sep:" \n" ); |
65 | let long: String = lines.join(sep:" \n" ); |
66 | |
67 | (Some(short), Some(long)) |
68 | } else { |
69 | let short: String = lines.join(sep:" \n" ); |
70 | let long: Option = force_long.then(|| short.clone()); |
71 | |
72 | (Some(short), long) |
73 | } |
74 | } |
75 | |
76 | #[cfg (not(feature = "unstable-markdown" ))] |
77 | fn split_paragraphs(lines: &[String]) -> Vec<String> { |
78 | use std::iter; |
79 | |
80 | let mut last_line: usize = 0; |
81 | iterimpl Iterator ::from_fn(|| { |
82 | let slice: &[String] = &lines[last_line..]; |
83 | let start: usize = slice.iter().position(|s| !is_blank(s)).unwrap_or(default:0); |
84 | |
85 | let slice: &[String] = &slice[start..]; |
86 | let len: usize = slice |
87 | .iter() |
88 | .position(|s| is_blank(s)) |
89 | .unwrap_or(default: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 | |
102 | fn remove_period(mut s: String) -> String { |
103 | if s.ends_with('.' ) && !s.ends_with(".." ) { |
104 | s.pop(); |
105 | } |
106 | s |
107 | } |
108 | |
109 | fn is_blank(s: &str) -> bool { |
110 | s.trim().is_empty() |
111 | } |
112 | |
113 | #[cfg (not(feature = "unstable-markdown" ))] |
114 | fn 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(sep:" " ) |
120 | } |
121 | |
122 | #[cfg (not(feature = "unstable-markdown" ))] |
123 | fn parse_markdown(lines: &[String]) -> (String, Option<String>) { |
124 | if lines.iter().any(|s: &String| is_blank(s)) { |
125 | let paragraphs: Vec = split_paragraphs(lines); |
126 | let short: String = paragraphs[0].clone(); |
127 | let long: String = paragraphs.join(sep:" \n\n" ); |
128 | (short, Some(long)) |
129 | } else { |
130 | let short: String = merge_lines(lines); |
131 | (short, None) |
132 | } |
133 | } |
134 | |
135 | #[cfg (feature = "unstable-markdown" )] |
136 | mod 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 inserted for each line. |
147 | prefix: String, |
148 | /// Should an empty line be inserted before the next anything. |
149 | hanging_paragraph: bool, |
150 | /// Are we in an empty line |
151 | 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 | // Reapplying all, because anstyle doesn't support merging styles |
209 | // (probably because the ambiguity around colors) |
210 | // TODO If we decide not to support any colors, we can replace this with |
211 | // anstyle::Effects and remove the need for applying them all individually. |
212 | 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 | // Markdown Configuration |
234 | let parsing_options = Options::ENABLE_STRIKETHROUGH; |
235 | // Minimal Styling for now, because we cannot configure it |
236 | 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) => { /* nothing to do */ } |
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) => { /* IGNORED */ } |
276 | Event::End(TagEnd::Image) => { /* IGNORED */ } |
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 | // split into lines to support code blocks |
339 | let mut lines = segment.lines(); |
340 | // `.lines()` always returns at least one |
341 | 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 | // There is not really anything useful to do with block level html. |
358 | Event::Html(html) => write!(writer, "{html}" ), |
359 | // At some point we could support custom tags like `<red>` |
360 | 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 | // Markdown features currently not supported |
371 | 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 | } |
411 | |