|
3 | 3 | //! #[derive(Parser)] works in terms of "paragraphs". Paragraph is a sequence of
|
4 | 4 | //! non-empty adjacent lines, delimited by sequences of blank (whitespace only) lines.
|
5 | 5 |
|
| 6 | +use std::fmt::Write; |
6 | 7 | use std::iter;
|
| 8 | +use std::ops::AddAssign; |
7 | 9 |
|
8 | 10 | pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String> {
|
9 | 11 | // multiline comments (`/** ... */`) may have LFs (`\n`) in them,
|
@@ -54,35 +56,137 @@ pub(crate) fn format_doc_comment(
|
54 | 56 | preprocess: bool,
|
55 | 57 | force_long: bool,
|
56 | 58 | ) -> (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 |
| - }; |
| 59 | + if preprocess { |
| 60 | + let (short, long) = parse_markdown(lines); |
| 61 | + let long = long.or_else(|| force_long.then(|| short.clone())); |
| 62 | + |
| 63 | + (Some(short), long) |
| 64 | + } else if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) { |
| 65 | + let short = lines[..first_blank].join("\n"); |
| 66 | + let long = lines.join("\n"); |
68 | 67 |
|
69 | 68 | (Some(short), Some(long))
|
70 | 69 | } 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 |
| - }; |
| 70 | + let short = lines.join("\n"); |
| 71 | + let long = force_long.then(|| short.clone()); |
81 | 72 |
|
82 | 73 | (Some(short), long)
|
83 | 74 | }
|
84 | 75 | }
|
85 | 76 |
|
| 77 | +fn parse_markdown(input: &[String]) -> (String, Option<String>) { |
| 78 | + use anstyle::Style; |
| 79 | + use pulldown_cmark::*; |
| 80 | + let input = input.join("\n"); |
| 81 | + let input = Parser::new_ext( |
| 82 | + &input, |
| 83 | + Options::ENABLE_STRIKETHROUGH | Options::ENABLE_SMART_PUNCTUATION, |
| 84 | + ); |
| 85 | + let mut text = String::new(); |
| 86 | + let heading = Style::new().bold(); |
| 87 | + let emphasis = Style::new().italic(); |
| 88 | + let strong = Style::new().bold(); |
| 89 | + let strike_through = Style::new().strikethrough(); |
| 90 | + let link = Style::new().underline(); |
| 91 | + let mut list_indices = Vec::new(); |
| 92 | + // let list_symbol = '●'; |
| 93 | + let list_symbol = '•'; |
| 94 | + let tab_width = 2; |
| 95 | + for event in input { |
| 96 | + match event { |
| 97 | + Event::Start(Tag::Paragraph) => { /* nothing to do */ } |
| 98 | + Event::Start(Tag::Heading { .. }) => write!(text, "{heading}").unwrap(), |
| 99 | + Event::Start( |
| 100 | + Tag::Image { .. } | Tag::BlockQuote(_) | Tag::CodeBlock(_) | Tag::HtmlBlock, |
| 101 | + ) => { /* IGNORED */ } |
| 102 | + Event::Start(Tag::List(list_start)) => { |
| 103 | + list_indices.push(list_start); |
| 104 | + if !text.is_empty() && !text.ends_with('\n') { |
| 105 | + text.push('\n'); |
| 106 | + } |
| 107 | + } |
| 108 | + Event::Start(Tag::Item) => { |
| 109 | + // TODO consider styling list |
| 110 | + write!(text, "{:1$}", "", tab_width * list_indices.len()).unwrap(); |
| 111 | + if let Some(Some(index)) = list_indices.last_mut() { |
| 112 | + write!(text, "{index}. ").unwrap(); |
| 113 | + index.add_assign(1); |
| 114 | + } else { |
| 115 | + write!(text, "{list_symbol} ").unwrap(); |
| 116 | + } |
| 117 | + } |
| 118 | + Event::Start(Tag::Emphasis) => write!(text, "{emphasis}").unwrap(), |
| 119 | + Event::Start(Tag::Strong) => write!(text, "{strong}").unwrap(), |
| 120 | + Event::Start(Tag::Strikethrough) => write!(text, "{strike_through}").unwrap(), |
| 121 | + Event::Start(Tag::Link { dest_url, .. }) => { |
| 122 | + write!(text, "\x1B]8;;{dest_url}\x1B\\{link}").unwrap(); |
| 123 | + } |
| 124 | + |
| 125 | + Event::End(TagEnd::Paragraph) => text.push_str("\n\n"), |
| 126 | + Event::End(TagEnd::Heading(..)) => write!(text, "{heading:#}\n\n").unwrap(), |
| 127 | + Event::End(TagEnd::List(_)) => { |
| 128 | + list_indices.pop().unwrap(); |
| 129 | + } |
| 130 | + Event::End(TagEnd::Item) => { |
| 131 | + if !text.ends_with('\n') { |
| 132 | + text.push('\n'); |
| 133 | + } |
| 134 | + } |
| 135 | + Event::End(TagEnd::Emphasis) => write!(text, "{emphasis:#}").unwrap(), |
| 136 | + Event::End(TagEnd::Strong) => write!(text, "{strong:#}").unwrap(), |
| 137 | + Event::End(TagEnd::Strikethrough) => write!(text, "{strike_through:#}").unwrap(), |
| 138 | + Event::End(TagEnd::Link) => write!(text, "\x1B]8;;\x1B\\{link:#}").unwrap(), |
| 139 | + Event::End( |
| 140 | + TagEnd::Image | TagEnd::BlockQuote(_) | TagEnd::HtmlBlock | TagEnd::CodeBlock, |
| 141 | + ) => { /* IGNORED */ } |
| 142 | + |
| 143 | + Event::Text(segment) => text.push_str(&segment), |
| 144 | + Event::Code(code) => { |
| 145 | + // TODO questionable |
| 146 | + write!(text, "{0}{code}{0:#}", Style::new().dimmed()).unwrap(); |
| 147 | + } |
| 148 | + // There is not really anything useful to do with block level html. |
| 149 | + Event::Html(html) => text.push_str(&html), |
| 150 | + // At some point we could support custom tags like `<red>` |
| 151 | + Event::InlineHtml(html) => text.push_str(&html), |
| 152 | + Event::SoftBreak => text.push(' '), |
| 153 | + Event::HardBreak => text.push('\n'), |
| 154 | + Event::Rule => { /* ignored */ } |
| 155 | + |
| 156 | + Event::Start( |
| 157 | + Tag::FootnoteDefinition(_) |
| 158 | + | Tag::DefinitionList |
| 159 | + | Tag::DefinitionListTitle |
| 160 | + | Tag::DefinitionListDefinition |
| 161 | + | Tag::Table(_) |
| 162 | + | Tag::TableHead |
| 163 | + | Tag::TableRow |
| 164 | + | Tag::TableCell |
| 165 | + | Tag::MetadataBlock(_), |
| 166 | + ) |
| 167 | + | Event::End( |
| 168 | + TagEnd::FootnoteDefinition |
| 169 | + | TagEnd::DefinitionList |
| 170 | + | TagEnd::DefinitionListTitle |
| 171 | + | TagEnd::DefinitionListDefinition |
| 172 | + | TagEnd::Table |
| 173 | + | TagEnd::TableHead |
| 174 | + | TagEnd::TableRow |
| 175 | + | TagEnd::TableCell |
| 176 | + | TagEnd::MetadataBlock(_), |
| 177 | + ) |
| 178 | + | Event::InlineMath(_) |
| 179 | + | Event::DisplayMath(_) |
| 180 | + | Event::FootnoteReference(_) |
| 181 | + | Event::TaskListMarker(_) => { |
| 182 | + unimplemented!("feature not enabled {event:?}") |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + let text = text.trim(); |
| 187 | + (text.to_owned(), Some(text.to_owned())) |
| 188 | +} |
| 189 | + |
86 | 190 | fn split_paragraphs(lines: &[String]) -> Vec<String> {
|
87 | 191 | let mut last_line = 0;
|
88 | 192 | iter::from_fn(|| {
|
|
0 commit comments