diff --git a/Cargo.lock b/Cargo.lock index e2215e278fa..4d2f4eafd17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,18 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-lossy" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934ff8719effd2023a48cf63e69536c1c3ced9d3895068f6f5cc9a4ff845e59b" +dependencies = [ + "anstyle", +] [[package]] name = "anstyle-parse" @@ -123,6 +132,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anstyle-svg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3607949e9f6de49ea4bafe12f5e4fd73613ebf24795e48587302a8cc0e4bb35" +dependencies = [ + "anstream", + "anstyle", + "anstyle-lossy", + "html-escape", + "unicode-width 0.2.0", +] + [[package]] name = "anstyle-wincon" version = "3.0.3" @@ -502,8 +524,10 @@ dependencies = [ name = "clap_derive" version = "4.5.24" dependencies = [ + "anstyle", "heck 0.5.0", "proc-macro2", + "pulldown-cmark", "quote", "syn", ] @@ -1156,6 +1180,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -2515,6 +2548,17 @@ dependencies = [ "nix 0.26.4", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags 2.6.0", + "memchr", + "unicase", +] + [[package]] name = "pure-rust-locales" version = "0.8.1" @@ -2976,6 +3020,7 @@ checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b" dependencies = [ "anstream", "anstyle", + "anstyle-svg", "content_inspector", "dunce", "escargot", @@ -2983,6 +3028,7 @@ dependencies = [ "libc", "normalize-line-endings", "os_pipe", + "serde_json", "similar", "snapbox-macros", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 66491476b3e..21e440ff451 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecat unstable-ext = ["clap_builder/unstable-ext"] unstable-styles = ["clap_builder/unstable-styles"] # deprecated unstable-derive-ui-tests = [] +unstable-markdown = ["clap_derive/unstable-markdown"] [lib] bench = false @@ -184,7 +185,7 @@ rustversion = "1.0.15" # Cutting out `filesystem` feature trycmd = { version = "0.15.3", default-features = false, features = ["color-auto", "diff", "examples"] } humantime = "2.1.0" -snapbox = "0.6.16" +snapbox = { version = "0.6.16", features = ["term-svg"] } shlex = "1.3.0" automod = "1.0.14" clap-cargo = { version = "0.15.0", default-features = false } diff --git a/Makefile b/Makefile index 93411604b58..4b902ad3c9f 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ _FEATURES_minimal = --no-default-features --features "std" _FEATURES_default = _FEATURES_wasm = --no-default-features --features "std help usage error-context suggestions" --features "deprecated derive cargo env unicode string" _FEATURES_full = --features "deprecated derive cargo env unicode string wrap_help unstable-ext" -_FEATURES_next = ${_FEATURES_full} --features unstable-v5 +_FEATURES_next = ${_FEATURES_full} --features "unstable-v5 unstable-markdown" _FEATURES_debug = ${_FEATURES_full} --features debug --features clap_complete/debug _FEATURES_release = ${_FEATURES_full} --release diff --git a/clap_derive/Cargo.toml b/clap_derive/Cargo.toml index 3de77477056..f22f825e34e 100644 --- a/clap_derive/Cargo.toml +++ b/clap_derive/Cargo.toml @@ -33,6 +33,8 @@ syn = { version = "2.0.8", features = ["full"] } quote = "1.0.9" proc-macro2 = "1.0.69" heck = "0.5.0" +pulldown-cmark = { version = "0.12.2", default-features = false, optional = true} +anstyle = {version ="1.0.10", optional = true} [features] default = [] @@ -40,6 +42,7 @@ debug = [] unstable-v5 = ["deprecated"] deprecated = [] raw-deprecated = ["deprecated"] +unstable-markdown = ["dep:pulldown-cmark", "dep:anstyle"] [lints] workspace = true diff --git a/clap_derive/src/utils/doc_comments.rs b/clap_derive/src/utils/doc_comments.rs index aeff594a1ee..59b3c4ec375 100644 --- a/clap_derive/src/utils/doc_comments.rs +++ b/clap_derive/src/utils/doc_comments.rs @@ -3,7 +3,8 @@ //! #[derive(Parser)] works in terms of "paragraphs". Paragraph is a sequence of //! non-empty adjacent lines, delimited by sequences of blank (whitespace only) lines. -use std::iter; +#[cfg(feature = "unstable-markdown")] +use markdown::parse_markdown; pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec { // multiline comments (`/** ... */`) may have LFs (`\n`) in them, @@ -54,36 +55,28 @@ pub(crate) fn format_doc_comment( preprocess: bool, force_long: bool, ) -> (Option, Option) { - if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) { - let (short, long) = if preprocess { - let paragraphs = split_paragraphs(lines); - let short = paragraphs[0].clone(); - let long = paragraphs.join("\n\n"); - (remove_period(short), long) - } else { - let short = lines[..first_blank].join("\n"); - let long = lines.join("\n"); - (short, long) - }; + if preprocess { + let (short, long) = parse_markdown(lines); + let long = long.or_else(|| force_long.then(|| short.clone())); + + (Some(remove_period(short)), long) + } else if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) { + let short = lines[..first_blank].join("\n"); + let long = lines.join("\n"); (Some(short), Some(long)) } else { - let (short, long) = if preprocess { - let short = merge_lines(lines); - let long = force_long.then(|| short.clone()); - let short = remove_period(short); - (short, long) - } else { - let short = lines.join("\n"); - let long = force_long.then(|| short.clone()); - (short, long) - }; + let short = lines.join("\n"); + let long = force_long.then(|| short.clone()); (Some(short), long) } } +#[cfg(not(feature = "unstable-markdown"))] fn split_paragraphs(lines: &[String]) -> Vec { + use std::iter; + let mut last_line = 0; iter::from_fn(|| { let slice = &lines[last_line..]; @@ -117,6 +110,7 @@ fn is_blank(s: &str) -> bool { s.trim().is_empty() } +#[cfg(not(feature = "unstable-markdown"))] fn merge_lines(lines: impl IntoIterator>) -> String { lines .into_iter() @@ -124,3 +118,289 @@ fn merge_lines(lines: impl IntoIterator>) -> String { .collect::>() .join(" ") } + +#[cfg(not(feature = "unstable-markdown"))] +fn parse_markdown(lines: &[String]) -> (String, Option) { + if lines.iter().any(|s| is_blank(s)) { + let paragraphs = split_paragraphs(lines); + let short = paragraphs[0].clone(); + let long = paragraphs.join("\n\n"); + (short, Some(long)) + } else { + let short = merge_lines(lines); + (short, None) + } +} + +#[cfg(feature = "unstable-markdown")] +mod markdown { + use anstyle::{Reset, Style}; + use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; + use std::fmt; + use std::fmt::Write; + use std::ops::AddAssign; + + #[derive(Default)] + struct MarkdownWriter { + output: String, + /// Prefix inserted for each line. + prefix: String, + /// Should an empty line be inserted before the next anything. + hanging_paragraph: bool, + /// Are we in an empty line + dirty_line: bool, + styles: Vec + + + + + This is a *fenced* code block. + + + + There is not much going on in terms of **styling**. + + + + --- + + + + Code blocks can also be initiated through + + Indentation. + + + + | This is a block quote. Regular styling should work here. + + | + + | even headings + + | + + | and regular paragraphs. + + | + + | - lists + + | - are + + | + + | 1. also + + | 1. supported + + | + + | | nesting them + + | | + + | | | also works (not) + + + + Usage: clap + + + + Options: + + -h, --help + + Print help (see a summary with '-h') + + + + + + diff --git a/tests/derive/snapshots/headers.term.svg b/tests/derive/snapshots/headers.term.svg new file mode 100644 index 00000000000..9bf671e0eda --- /dev/null +++ b/tests/derive/snapshots/headers.term.svg @@ -0,0 +1,53 @@ + + + + + + + This is a header + + + + second level + + + + additional styling on top of it + + + + regular paragraph + + + + Usage: clap + + + + Options: + + -h, --help + + Print help + + + + + + diff --git a/tests/derive/snapshots/html.term.svg b/tests/derive/snapshots/html.term.svg new file mode 100644 index 00000000000..53edac4b2df --- /dev/null +++ b/tests/derive/snapshots/html.term.svg @@ -0,0 +1,53 @@ + + + + + + + <html> + + <is> + + <used> + + </verbatim> + + </html> + + + + + + <inline>html</as-well> + + + + Usage: clap + + + + Options: + + -h, --help + + Print help + + + + + + diff --git a/tests/derive/snapshots/inline_styles.term.svg b/tests/derive/snapshots/inline_styles.term.svg new file mode 100644 index 00000000000..8600d14c6be --- /dev/null +++ b/tests/derive/snapshots/inline_styles.term.svg @@ -0,0 +1,45 @@ + + + + + + + emphasis bold strike through code + + + + all of them combined in one line + + + + Usage: clap + + + + Options: + + -h, --help + + Print help (see a summary with '-h') + + + + + + diff --git a/tests/derive/snapshots/links.term.svg b/tests/derive/snapshots/links.term.svg new file mode 100644 index 00000000000..b17523bdbd0 --- /dev/null +++ b/tests/derive/snapshots/links.term.svg @@ -0,0 +1,51 @@ + + + + + + + https://example.com/literal + + + + with name + + + + image + + + + referencing + + + + Usage: clap + + + + Options: + + -h, --help + + Print help (see a summary with '-h') + + + + + + diff --git a/tests/derive/snapshots/lists.term.svg b/tests/derive/snapshots/lists.term.svg new file mode 100644 index 00000000000..4b8b0945dbc --- /dev/null +++ b/tests/derive/snapshots/lists.term.svg @@ -0,0 +1,67 @@ + + + + + + + Lists: + + + + - unordered + + - bullet + + - lists + + - with multiple + + - levels + + + + 0. numeric lists + + 1. only care + + 1. about the initial number + + 2. + + 5. and count from there + + 6. anything goes + + 3. though they need an empty line + + + + Usage: clap + + + + Options: + + -h, --help + + Print help (see a summary with '-h') + + + + + + diff --git a/tests/derive/snapshots/paragraphs.term.svg b/tests/derive/snapshots/paragraphs.term.svg new file mode 100644 index 00000000000..3a189315eae --- /dev/null +++ b/tests/derive/snapshots/paragraphs.term.svg @@ -0,0 +1,65 @@ + + + + + + + Paragraphs are separated by empty lines. All lines will be joined onto one. + + + + The first paragraph is used as short help by clap. + + backslashes can be used to insert hard line breaks. + + + + | these | can | + + | ----- | ----- | + + | be | used | + + | for | tables| + + + + Because tables are not yet supported. + + + + You can also use trailing spaces for hard breaks, + + but this is not really recommended. + + + + Usage: clap + + + + Options: + + -h, --help + + Print help (see a summary with '-h') + + + + + +