Skip to content

Commit

Permalink
Support multiline docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Timmmm committed Nov 16, 2023
1 parent 6149a1a commit f8b5bca
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 20 deletions.
34 changes: 32 additions & 2 deletions argh/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,49 @@ fn multiline_doc_comment_description() {
/// that is spread across
/// a number of
/// lines of comments.
///
/// It can have multiple paragraphs
/// too and those should be
/// collapsed into one line and
/// reflowed.
///
/// * It can also: have lists
/// * That are: not reflowed
///
/// | And | Tables |
/// |------|--------|
/// | work | too |
///
/// The basic rule is that lines that start with
/// an alphabetic character (a-zA-Z) are joined
/// to the previous line.
_s: bool,
}

// The \x20s are so that editors don't strip the trailing spaces.
assert_help_string::<Cmd>(
r###"Usage: test_arg_0 [--s]
"Usage: test_arg_0 [--s]
Short description
Options:
--s a switch with a description that is spread across a number
of lines of comments.
\x20
It can have multiple paragraphs too and those should be
collapsed into one line and reflowed.
\x20
* It can also: have lists
* That are: not reflowed
\x20
| And | Tables |
|------|--------|
| work | too |
\x20
The basic rule is that lines that start with an alphabetic
character (a-zA-Z) are joined to the previous line.
--help, help display usage information
"###,
",
);
}

Expand Down
37 changes: 34 additions & 3 deletions argh_derive/src/parse_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,36 @@ fn parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut V
}
}

/// Extend the string `value` with the `new_line`. `new_line` is expected to
/// not end with a newline.
///
/// This is used for constructing a description of some command/options from
/// its docstring by processing the docstring lines one at a time (because
/// single-line docstring comments get converted to separate attributes).
///
/// The logic implemented here is intended to join paragraphs into a single
/// line, while leaving other content like indented code, lists, tables, etc.
/// untouched. Roughly like Markdown.
///
fn extend_docstring(doc: &mut String, new_line: &str) {
// Skip the space immediately after the `///` if present.
let new_line = new_line.strip_prefix(' ').unwrap_or(new_line);

if new_line.starts_with(|c: char| c.is_alphanumeric()) {
if !doc.is_empty() {
if doc.ends_with('\n') {
doc.push('\n');
} else {
doc.push(' ');
}
}
} else {
doc.push('\n');
}

doc.push_str(new_line);
}

fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Description>) {
let nv = if let Some(nv) = errors.expect_meta_name_value(&attr.meta) {
nv
Expand All @@ -574,9 +604,10 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc

if let Some(lit_str) = errors.expect_lit_str(&nv.value) {
let lit_str = if let Some(previous) = slot {
let previous = &previous.content;
let previous_span = previous.span();
syn::LitStr::new(&(previous.value() + &unescape_doc(lit_str.value())), previous_span)
let mut doc = previous.content.value();
extend_docstring(&mut doc, &unescape_doc(lit_str.value()));
// This is N^2 unfortunately but hopefully N won't become too large!
syn::LitStr::new(&doc, previous.content.span())
} else {
syn::LitStr::new(&unescape_doc(lit_str.value()), lit_str.span())
};
Expand Down
32 changes: 17 additions & 15 deletions argh_shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,24 +142,26 @@ pub fn write_description(out: &mut String, cmd: &CommandInfo<'_>) {
new_line(&mut current_line, out);
}

let mut words = cmd.description.split(' ').peekable();
while let Some(first_word) = words.next() {
indent_description(&mut current_line);
current_line.push_str(first_word);

'inner: while let Some(&word) = words.peek() {
if (char_len(&current_line) + char_len(word) + 1) > WRAP_WIDTH {
new_line(&mut current_line, out);
break 'inner;
} else {
// advance the iterator
let _ = words.next();
current_line.push(' ');
current_line.push_str(word);
for line in cmd.description.lines() {
let mut words = line.split(' ').peekable();
while let Some(first_word) = words.next() {
indent_description(&mut current_line);
current_line.push_str(first_word);

while let Some(&word) = words.peek() {
if (char_len(&current_line) + char_len(word) + 1) > WRAP_WIDTH {
new_line(&mut current_line, out);
break;
} else {
// advance the iterator
let _ = words.next();
current_line.push(' ');
current_line.push_str(word);
}
}
}
new_line(&mut current_line, out);
}
new_line(&mut current_line, out);
}

// Indent the current line in to DESCRIPTION_INDENT chars.
Expand Down

0 comments on commit f8b5bca

Please sign in to comment.