Skip to content

Commit

Permalink
Support multiline docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Timmmm committed Jul 15, 2022
1 parent b9296f0 commit b2c5859
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 21 deletions.
38 changes: 34 additions & 4 deletions argh/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,23 +213,53 @@ fn multiline_doc_comment_description() {
/// Short description
struct Cmd {
#[argh(switch)]
/// a switch with a description
/// A switch with a 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
--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 display usage information
"###,
",
);
}

Expand Down
41 changes: 38 additions & 3 deletions argh_derive/src/parse_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,40 @@ 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 = match new_line.bytes().next() {
Some(b' ') => &new_line[1..],
_ => new_line,
};
let line_starts_with_letter = new_line.bytes().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false);
if line_starts_with_letter {
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) = attr_to_meta_name_value(errors, attr) {
nv
Expand All @@ -520,9 +554,10 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc

if let Some(lit_str) = errors.expect_lit_str(&nv.lit) {
let lit_str = if let Some(previous) = slot {
let previous = &previous.content;
let previous_span = previous.span();
syn::LitStr::new(&(previous.value() + &*lit_str.value()), previous_span)
let mut doc = previous.content.value();
extend_docstring(&mut 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 {
lit_str.clone()
};
Expand Down
30 changes: 16 additions & 14 deletions argh_shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,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);
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);

'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);
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 b2c5859

Please sign in to comment.