Repository: clap-rs/clap (language: rust)

Issue:
Clap derive should parse markdown doc comment into normal text
Maintainer's notes:
- Workaround: `verbatim_doc_comment` attribute
- `unstable-markdown` feature is being tracked in #5900

---
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the existing issues

### Rust Version

rustc 1.48.0 (7eac88abb 2020-11-16)

### Clap Version

3.0.0-beta.2

### Minimal reproducible code

```rust
use clap::Clap;

#[derive(Clap, Debug)]
#[clap(name = "xh")]
struct Cli {
    /// Optional key-value pairs to be included in the request.
    ///
    /// - key:=value to add a complex JSON value (e.g. `numbers:=[1,2,3]`)
    /// - key@filename to upload a file from filename (with --form)
    /// - header:value to add a header
    /// - header: to unset a header
    /// - header; to add a header with an empty value
    ///
    /// A backslash can be used to escape special characters (e.g. weird\:key=value).
    #[clap(value_name = "REQUEST_ITEM")]
    raw_rest_args: Vec<String>
}

fn main() {
      let _ = Cli::parse();
}
```


### Steps to reproduce the bug with the above code
```
cargo run -- --help
```

### Actual Behaviour
```
ARGS:
    <REQUEST_ITEM>...
            Optional key-value pairs to be included in the request.

            * key==value to add a parameter to the URL \n * key=value to add a JSON field (--json)
            or form field (--form) * key:=value to add a complex JSON value (e.g.
            `numbers:=[1,2,3]`) * key@filename to upload a file from filename (with --form) *
            header:value to add a header * header: to unset a header * header; to add a header with
            an empty value

            A backslash can be used to escape special characters (e.g. weird\:key=value).
```
### Expected Behaviour
```
ARGS:
    <REQUEST_ITEM>...
            Optional key-value pairs to be included in the request.

            * key==value to add a parameter to the URL
            * key=value to add a JSON field (--json) or form field (--form)
            * key:=value to add a complex JSON value (e.g. `numbers:=[1,2,3]`)
            * key@filename to upload a file from filename (with --form)
            * header:value to add a header
            * header: to unset a header
            * header; to add a header with an empty value

            A backslash can be used to escape special characters (e.g. weird\:key=value).
```
### Additional Context

In StructOpt, it possible to use the `long_help` attribute to preserve newlines but ~~I couldn't find something similar in clap-derive~~.

Just found that it is called `long_about` in clap-derive.

### Debug Output

Will add if requested


Discussion / hints:
There is quite a bit of discussion in #2401 
One of the challenges with this is we'd basically need to pull in pulldown-cmark which is quite a large dependency.  This would need to be put behind a feature flag.
My main goal was to preserve newlines in the doc comment and that should be achievable with the `verbatim_doc_comment` attribute.

#### Example

```rust
// clap = { version = "3.0.0-rc.4", features = ["derive"] }

use clap::Parser;

#[derive(Parser, Debug)]
#[clap(name = "xh")]
struct Cli {
    /// Optional key-value pairs to be included in the request.
    ///
    ///   • key:=value to add a complex JSON value (e.g. `numbers:=[1,2,3]`)
    ///   • key@filename to upload a file from filename (with --form)
    ///   • header:value to add a header
    ///   • header: to unset a header
    ///   • header; to add a header with an empty value
    ///
    /// A backslash can be used to escape special characters (e.g. weird\:key=value).
    #[clap(value_name = "REQUEST_ITEM", verbatim_doc_comment)]
    raw_rest_args: Vec<String>,
}

fn main() {
    let _ = Cli::parse();
}

```

#### Output
```
USAGE:
    temp-clap [REQUEST_ITEM]...

ARGS:
    <REQUEST_ITEM>...
            Optional key-value pairs to be included in the request.

              • key:=value to add a complex JSON value (e.g. `numbers:=[1,2,3]`)
              • key@filename to upload a file from filename (with --form)
              • header:value to add a header
              • header: to unset a header
              • header; to add a header with an empty value

            A backslash can be used to escape special characters (e.g. weird\:key=value).

OPTIONS:
    -h, --help
            Print help information
```

I wouldn't mind if this issue was to be closed.
I think its still an interesting topic for us to weigh out.  I did call out the workaround in the issue so its more discoverable.
We may need only a subset of markdown features in CLI parser:

- newline escaping (implemented in #3228)
- lists as suggested here (should be relatively easy to implement, even with nesting)
- bold, italic, bold-italic, inline code - if colors enabled (not sure whether it's worth effort, but why not?)
- fenced code blocks (without syntax highlighting, of course)

There also could be some mechanism to cut off help message from doc comment as @epage suggested [here](https://github.com/clap-rs/clap/issues/3198#issuecomment-1002154924) — IMO markdown headers are a perfect mechanism for that e.g.:

- without any header everything goes to -h --help output
  - that's the same as if everything was placed under `# Help` header
- if `# Long help` is specified that section will be appended to --help output (only)
- we may also have `# Manpage` section (if clap will provide some mechanism for manpage generation)
- text under all other headers will be ignored by clap

---

I think this subset should satisfy 99.9% of all use cases of clap, everyone should like it, and it doesn't seem to be so complicated to require pulldown-cmark or any other heavy dependency.
My experience writing one off parsers has made me a bit cautious of doing so.  I threw together some [md parser benchmarks](https://github.com/epage/md-benchmarks-rs) to see what might be small enough for being an optional dependency.  `minimad` looks tempting.
Another thought on markdown support, should we support it at runtime, compile time, or both?

The core of this Issue is our line breaks which is a problem exclusive to the derive API.  For that, we could build markdown parsing directly into `clap_derive`.

Another step up is markdown formatting.  For that, we'd want to expose it to both APIs.  Say we generalized our `Colorizer` struct and accepted it for all our user-facing inputs (for #1790, #1433), users can do whatever styling they want, though it might be a bit arduous.  We then provided a markdown-parsing macro that would code-generate `Colorizer`.  `clap_derive` would then have this built-in for all doc comments.

The main benefit of this is that this would remove the binary-size overhead of the markdown parser.  We'd still need to compile it (though it'd be optional).  Though this would mean we wouldn't regress in `--help` and error performance by introducing markdown, the performance would have to be pretty bad for us to care.

This has the added benefit that we wouldn't need yet-another Setting in the API for doing controlling this at runtime.

Another question for us to answer in this issue is how disruptive would markdown parsing be.   While `--help`s output is not considered "stable", we shouldn't dramatically alter users crafting of their help output.  To what degree of markdown parsing can we add in a patch, a feature release, or a breaking release?
>Another thought on markdown support, should we support it at runtime, compile time, or both?

Compile time only sounds reasonable to me (but I only use the derive API, so I guess I would say that 😁) .

> Another question for us to answer in this issue is how disruptive would markdown parsing be.

I _think_ that (as a user) if markdown parsing were turned on for existing help text that would probably break a bunch of people, but if it was behind a flag, or minor updates to the markdown output were later made in minor/patch updates, that would probably be fine, as what was intended to be output would still be there, just maybe looking slightly prettier.

>The core of this Issue is our line breaks which is a problem exclusive to the derive API. For that, we could build markdown parsing directly into clap_derive.

For what it's worth there are a bunch of other things I would use if we had markdown support, not least:

1. _Italic_ and **bold** `inline code`
2. Subheadings and code blocks, e.g.:
   ````markdown
   ## EXAMPLES: <!-- Should be highlighted yellow the same way the other sections in the generated help like USAGE: are today. -->

   <!-- Syntax highlighting in examples would be really neat. -->
   ```console
   # Run the subcommand with the arg specified.
   ❯ my-command my-subcommand --my-arg
   ```
   ````
3. docs.rs compatibility -> If you have a binary/library combo that takes a set of options, it's nice to be able to use the same documentation for docs.rs and for `mycmd --help`.
> `<!-- Syntax highlighting in examples would be really neat. -->`

I was hoping someone wasn't going to say that :)

syntect is the main go-to for syntax highlighting.    It can be a bit slow to compile and big.  It can be made optional.    Looks like we can also disable the built-in language / theme set.  I doubt we need all of them.  The question is what languages do we need at minimum and how should we respond *if* someone asks for another language.

We'll need to do some analysis to decide how worth it this is vs [generating help at build-time](https://github.com/clap-rs/clap/issues/2914)
One design problem I've been running into is how to render markdown at compile time when the colors aren't known until runtime.

I think the way to do this is to allow a `fn(fmt, palette) -> fmt::Result` to be passed into `StyledStr` and the derive would generate this, parsing the markdown at compile time but rendering the styles at runtime.
I'd only ask for italic and bold for basic formatting.
In my case I just want to add a space in a list without having to add a newline between the bullets and without having to use verbatim doc comments (so wrap_help is respected properly)
Is there some specification on how this should work? Should we fully parse markdown, or should we just implement the parts that make sense, i.e., lists, bold, italic? I'm currently quite annoyed by lists not working so I'd be willing to invest some time into implementing some solution here.
How established is CommonMark in Rust docs contexts? I would suggest pulldown-cmark or, if another Markdown parser is established in Rust context whatever that is, as a full flow Markdown parser makes more sense than a "whatever makes sense/minimalist" approach. Markdown is notoriously hard to actually get right even if small things like emphasis or list parsing.
Subsets of markdown were covered earlier in the thread
https://github.com/clap-rs/clap/issues/2389#issuecomment-1002370465


https://github.com/clap-rs/clap/issues/2389#issuecomment-1002673827 you also cautioned against writing a custom parser should we just use pulldown-cmark and ignore the ones we don't support?

Also should this be configurable, for some the mapping to ANSI is pretty clear e.g. **bold** or *italic* while for others it's unclear e.g. `code_block` should that set a backgroun? or keep the quotes ` ?

Another thing to consider would be should support be implemented for html tags like in color_print: https://docs.rs/color-print/latest/color_print/index.html#list-of-accepted-tags
> https://github.com/clap-rs/clap/issues/2389#issuecomment-1002673827 you also cautioned against writing a custom parser should we just use pulldown-cmark and ignore the ones we don't support?

Looking at [md parser benchmarks](https://github.com/epage/md-benchmarks-rs),  I would be curious about how well we could get by with minimad.

Details like `code_lock` would need to be figured out.  In a way, it is another form of literal.

It would be good to have a way for people to set colors but I would be worried about full HTML support.
> It would be good to have a way for people to set colors but I would be worried about full HTML support.

The only reason I mentioned `color_print` is that it is referenced in the clap docs, and it could be nice to also be able to use it inside of markdown.

I would agree that any block elements would be annoying to deal with, so only inline ones could work. Maybe adding some tags for all the styles definable could be useful, i.e., `<header>`, `<error>`, `usage`, ...

> Details like `code_block` would need to be figured out. In a way, it is another form of literal.

Makes sense, though one could also add the different markdown elements to `Styles`, e.g., if someone wants to color their bold text red. 

The other question is should mark down parsing be enabled at runtime as well? If not, the compile time parsed markdown needs to be converted like you suggested here:
> One design problem I've been running into is how to render markdown at compile time when the colors aren't known until runtime.
> I think the way to do this is to allow a fn(fmt, palette) -> fmt::Result to be passed into StyledStr and the derive would generate this, parsing the markdown at compile time but rendering the styles at runtime.

Even if we support markdown parsing at runtime, one might like to move that effort to compilation where possible, but we could also just implement it for runtime first and then check if there is a relevant performance penalty end disable it if so.

The whole markdown feature should probably be behind a feature flag anyway.

Would you be in favor of building a PoC for markdown parsing to test it out?
I suspect we should do this all at compile time though either direction comes with its problems.

One combination of challenges
- I intentionally switched `StyledStr` to only hold ANSI escape codes
- If we want to provide style classes like "header",  the conversion of that to ANSI is only really available at runtime

iirc there were more.  Feel free to play around and come back with more ideas.  I feel like I'd want things settled a little more before adding an unstable feature for this.
I found two issues with minimad, doesn't support lists using `-` and doesn't support multilevel list, because it thinks they are codeblocks https://github.com/Canop/minimad/issues/2.

While multilevel lists probably aren't as important, the list style could be quite confusing and result in unnecessary issues :D

```md
- Not recognised as list
* Is a list
    * is a code block
```
results in:
```rust
Text {
    lines: [
        Normal(
            Composite {
                style: Paragraph,
                compounds: [],
            },
        ),
        Normal(
            Composite {
                style: Paragraph,
                compounds: [
                    "- Not recognised as list",
                ],
            },
        ),
        Normal(
            Composite {
                style: ListItem(
                    0,
                ),
                compounds: [
                    "Is a list",
                ],
            },
        ),
        Normal(
            Composite {
                style: Code,
                compounds: [
                    "* is a code block",
                ],
            },
        ),
    ],
}
```

I'm gonna experiment with pulldown, but shouldn't be too bad to switch that later on
I wonder if they'd be open to adding more list styles.
> I wonder if they'd be open to adding more list styles.

Might be, but the issue was open for 4 years, and I was more interested in the other parts of this, but I think the main considerations are independent of parser and should be therefor easy enough to replace.
Just because I looked it up, I'll note for context that rustdoc uses pulldown-cmark, so that would be the most directly compatible.
Created an initial PoC https://github.com/clap-rs/clap/pull/5891

![Image](https://github.com/user-attachments/assets/321f847b-6f33-47f8-9650-6b8d45e2dacc)

Still some things to figure out, like should we use Unicode characters for list items? And which styles should we apply as we don't have access to the actual styles.

One thing I also need to fix is that `anstyle` fully resets the style when ending e.g. bold, so I need to restart italic manually.
I know at least Cargo is cautious on the use of unicode, auto-detecting when terminals should support it with a config to override it.
> I know at least Cargo is cautious on the use of unicode, auto-detecting when terminals should support it with a config to override it.

As that'd be hard to do at compile time, should we only output ASCII for now? The only alternative would be to generate 2 help messages and then in the generated builder switch between them depending on Unicode capability.
Short works as expected, i.e., only taking the first paragraph:

![Image](https://github.com/user-attachments/assets/70664c24-9d8d-4e8c-a787-c698dfecf737)

Long takes everything

![Image](https://github.com/user-attachments/assets/ded494de-c2a3-4613-8f3e-5b2d55647f5b)

@epage Should we also investigate using headings for separating sections and if so what would be the names? `# Help` and `# Long help` (probably matching any casing)? 

Following https://github.com/clap-rs/clap/issues/2389#issuecomment-1002370465 the implementation would be as follows:

1. if the doc comment contains no top-level heading with the content `help` (any casing) then follow regular procedure, i.e. take first paragraph as `help` and everything as `long_help`.
2. if the doc comment contains a top-level `help` heading take everything below it and use it for `help` if there is also a `long help` heading take everything below `help` and `long help` and use it for the long help.

This leaves the following questions for me:
1. What if the doc comment only contains `# Long Help`?

And the following alternative implementations
1. We could instead of `# Long Help` use a sub heading of `# Help` called `## Long`.
2. We could (if there is no `# Long Help`) apply the default parsing behavior to the content of `# Help` i.e. use the first paragraph for `help` and everything for `long_help`.
3. Should we, if `# Long Help` is specified only output this section as the complete text for `long_help`? This could be helpful if the short help is redundant when the `long_help` is also printed. In this case we could use `# Long Help` to contain the complete `long_help` and use the `## Long (Help)` below `# Help` to have the behavior of everything before `## Long` is only contained in `help` while `long_help` would contain everything in the `# Help` section including `## Long`.
btw there was one test where the output changed because of this, as now `` ` `` is dropped in the output.
imo any handling of `# Help` and `# Long Help` would be a breaking change and doesn't need to be handled with improved markdown handling, so I'd recommend someone open a dedicated issue on it once we have this.
> imo any handling of `# Help` and `# Long Help` would be a breaking change and doesn't need to be handled with improved markdown handling, so I'd recommend someone open a dedicated issue on it once we have this.

Agreed, solves a very different problem to simple markdown parsing as well.
@epage I did document the styling in the PR-description (#5891). Feel free to expand on it.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4bd2cbe785a..f99a1440d82 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"
@@ -1431,7 +1464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
 dependencies = [
  "cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -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 59c8bed0f28..ae9b9618639 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.14.1", 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<String> {
     // multiline comments (`/** ... */`) may have LFs (`\n`) in them,
@@ -54,36 +55,28 @@ pub(crate) fn format_doc_comment(
     preprocess: bool,
     force_long: bool,
 ) -> (Option<String>, Option<String>) {
-    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<String> {
+    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<Item = impl AsRef<str>>) -> String {
     lines
         .into_iter()
@@ -124,3 +118,289 @@ fn merge_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String {
         .collect::<Vec<_>>()
         .join(" ")
 }
+
+#[cfg(not(feature = "unstable-markdown"))]
+fn parse_markdown(lines: &[String]) -> (String, Option<String>) {
+    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<Style>,
+    }
+
+    impl MarkdownWriter {
+        fn newline(&mut self) {
+            self.reset();
+            self.output.push('\n');
+            self.dirty_line = false;
+        }
+        fn endline(&mut self) {
+            if self.dirty_line {
+                self.newline();
+            }
+        }
+        fn new_paragraph(&mut self) {
+            self.endline();
+            self.hanging_paragraph = true;
+        }
+
+        fn write_fmt(&mut self, arguments: fmt::Arguments<'_>) {
+            if self.hanging_paragraph {
+                self.hanging_paragraph = false;
+                self.newline();
+            }
+            if !self.dirty_line {
+                self.output.push_str(&self.prefix);
+                self.apply_styles();
+                self.dirty_line = true;
+            }
+            self.output.write_fmt(arguments).unwrap();
+        }
+
+        fn start_link(&mut self, dest_url: pulldown_cmark::CowStr<'_>) {
+            write!(self, "\x1B]8;;{dest_url}\x1B\\");
+        }
+        fn end_link(&mut self) {
+            write!(self, "\x1B]8;;\x1B\\");
+        }
+
+        fn start_style(&mut self, style: Style) {
+            self.styles.push(style);
+            write!(self, "{style}");
+        }
+        fn end_style(&mut self, style: Style) {
+            let last_style = self.styles.pop();
+            debug_assert_eq!(last_style.unwrap(), style);
+
+            write!(self, "{Reset}");
+            self.apply_styles();
+        }
+
+        fn reset(&mut self) {
+            write!(self, "{Reset}");
+        }
+
+        fn apply_styles(&mut self) {
+            // Reapplying all, because anstyle doesn't support merging styles
+            // (probably because the ambiguity around colors)
+            // TODO If we decide not to support any colors, we can replace this with
+            // anstyle::Effects and remove the need for applying them all individually.
+            for style in &self.styles {
+                write!(self.output, "{style}").unwrap();
+            }
+        }
+
+        fn remove_prefix(&mut self, quote_prefix: &str) {
+            debug_assert!(self.prefix.ends_with(quote_prefix));
+            let new_len = self.prefix.len() - quote_prefix.len();
+            self.prefix.truncate(new_len);
+        }
+
+        fn add_prefix(&mut self, quote_prefix: &str) {
+            if self.hanging_paragraph {
+                self.hanging_paragraph = false;
+                self.newline();
+            }
+            self.prefix += quote_prefix;
+        }
+    }
+
+    pub(super) fn parse_markdown(input: &[String]) -> (String, Option<String>) {
+        // Markdown Configuration
+        let parsing_options = Options::ENABLE_STRIKETHROUGH;
+        // Minimal Styling for now, because we cannot configure it
+        let style_heading = Style::new().bold().underline();
+        let style_emphasis = Style::new().italic();
+        let style_strong = Style::new().bold();
+        let style_strike_through = Style::new().strikethrough();
+        let style_link = Style::new().underline();
+        let style_code = Style::new().bold();
+        let list_symbol = '-';
+        let quote_prefix = "| ";
+        let indentation = "  ";
+
+        let input = input.join("\n");
+        let input = Parser::new_ext(&input, parsing_options);
+
+        let mut short = None;
+        let mut has_details = false;
+
+        let mut writer = MarkdownWriter::default();
+
+        let mut list_indices = Vec::new();
+
+        for event in input {
+            if short.is_some() {
+                has_details = true;
+            }
+            match event {
+                Event::Start(Tag::Paragraph) => { /* nothing to do */ }
+                Event::End(TagEnd::Paragraph) => {
+                    if short.is_none() {
+                        short = Some(writer.output.trim().to_owned());
+                    }
+                    writer.new_paragraph();
+                }
+
+                Event::Start(Tag::Heading { .. }) => writer.start_style(style_heading),
+                Event::End(TagEnd::Heading(..)) => {
+                    writer.end_style(style_heading);
+                    writer.new_paragraph();
+                }
+
+                Event::Start(Tag::Image { .. } | Tag::HtmlBlock) => { /* IGNORED */ }
+                Event::End(TagEnd::Image) => { /* IGNORED */ }
+                Event::End(TagEnd::HtmlBlock) => writer.new_paragraph(),
+
+                Event::Start(Tag::BlockQuote(_)) => writer.add_prefix(quote_prefix),
+                Event::End(TagEnd::BlockQuote(_)) => {
+                    writer.remove_prefix(quote_prefix);
+                    writer.new_paragraph();
+                }
+
+                Event::Start(Tag::CodeBlock(_)) => {
+                    writer.add_prefix(indentation);
+                    writer.start_style(style_code);
+                }
+                Event::End(TagEnd::CodeBlock) => {
+                    writer.remove_prefix(indentation);
+                    writer.end_style(style_code);
+                    writer.dirty_line = false;
+                    writer.hanging_paragraph = true;
+                }
+
+                Event::Start(Tag::List(list_start)) => {
+                    list_indices.push(list_start);
+                    writer.endline();
+                }
+                Event::End(TagEnd::List(_)) => {
+                    let list = list_indices.pop();
+                    debug_assert!(list.is_some());
+                    if list_indices.is_empty() {
+                        writer.new_paragraph();
+                    }
+                }
+                Event::Start(Tag::Item) => {
+                    if let Some(Some(index)) = list_indices.last_mut() {
+                        write!(writer, "{index}. ");
+                        index.add_assign(1);
+                    } else {
+                        write!(writer, "{list_symbol} ");
+                    }
+                    writer.add_prefix(indentation);
+                }
+                Event::End(TagEnd::Item) => {
+                    writer.remove_prefix(indentation);
+                    writer.endline();
+                }
+
+                Event::Start(Tag::Emphasis) => writer.start_style(style_emphasis),
+                Event::End(TagEnd::Emphasis) => writer.end_style(style_emphasis),
+                Event::Start(Tag::Strong) => writer.start_style(style_strong),
+                Event::End(TagEnd::Strong) => writer.end_style(style_strong),
+                Event::Start(Tag::Strikethrough) => writer.start_style(style_strike_through),
+                Event::End(TagEnd::Strikethrough) => writer.end_style(style_strike_through),
+
+                Event::Start(Tag::Link { dest_url, .. }) => {
+                    writer.start_link(dest_url);
+                    writer.start_style(style_link);
+                }
+                Event::End(TagEnd::Link) => {
+                    writer.end_link();
+                    writer.end_style(style_link);
+                }
+
+                Event::Text(segment) => {
+                    // split into lines to support code blocks
+                    let mut lines = segment.lines();
+                    // `.lines()`  always returns at least one
+                    write!(writer, "{}", lines.next().unwrap());
+                    for line in lines {
+                        writer.endline();
+                        write!(writer, "{line}");
+                    }
+                    if segment.ends_with('\n') {
+                        writer.endline();
+                    }
+                }
+
+                Event::Code(code) => {
+                    writer.start_style(style_code);
+                    write!(writer, "{code}");
+                    writer.end_style(style_code);
+                }
+
+                // There is not really anything useful to do with block level html.
+                Event::Html(html) => write!(writer, "{html}"),
+                // At some point we could support custom tags like `<red>`
+                Event::InlineHtml(html) => write!(writer, "{html}"),
+                Event::SoftBreak => write!(writer, " "),
+                Event::HardBreak => writer.endline(),
+
+                Event::Rule => {
+                    writer.new_paragraph();
+                    write!(writer, "---");
+                    writer.new_paragraph();
+                }
+
+                // Markdown features currently not supported
+                Event::Start(
+                    Tag::FootnoteDefinition(_)
+                    | Tag::DefinitionList
+                    | Tag::DefinitionListTitle
+                    | Tag::DefinitionListDefinition
+                    | Tag::Table(_)
+                    | Tag::TableHead
+                    | Tag::TableRow
+                    | Tag::TableCell
+                    | Tag::MetadataBlock(_),
+                )
+                | Event::End(
+                    TagEnd::FootnoteDefinition
+                    | TagEnd::DefinitionList
+                    | TagEnd::DefinitionListTitle
+                    | TagEnd::DefinitionListDefinition
+                    | TagEnd::Table
+                    | TagEnd::TableHead
+                    | TagEnd::TableRow
+                    | TagEnd::TableCell
+                    | TagEnd::MetadataBlock(_),
+                )
+                | Event::InlineMath(_)
+                | Event::DisplayMath(_)
+                | Event::FootnoteReference(_)
+                | Event::TaskListMarker(_) => {
+                    unimplemented!("feature not enabled {event:?}")
+                }
+            }
+        }
+        let short = short.unwrap_or_else(|| writer.output.trim_end().to_owned());
+        let long = writer.output.trim_end();
+        let long = has_details.then(|| long.to_owned());
+        (short, long)
+    }
+}


Test patch:
diff --git a/tests/derive/markdown.rs b/tests/derive/markdown.rs
new file mode 100644
index 00000000000..045ca437fe7
--- /dev/null
+++ b/tests/derive/markdown.rs
@@ -0,0 +1,145 @@
+#![cfg(feature = "unstable-markdown")]
+
+use clap::CommandFactory;
+use clap_derive::Parser;
+use snapbox::file;
+
+macro_rules! assert_help {
+    ($Command:ty, $filename:literal) => {{
+        let help = <$Command>::command().render_long_help().ansi().to_string();
+        snapbox::assert_data_eq!(help, file![$filename]);
+    }};
+}
+
+#[test]
+fn headers() {
+    /// # This is a header
+    /// ## second level
+    /// ### `additional` *styling **on ~top~ of** it*
+    /// regular paragraph
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/headers.term.svg");
+}
+
+#[test]
+fn inline_styles() {
+    /// *emphasis* **bold** ~strike through~ `code`
+    ///
+    /// *all **of ~them `combined` in~ one** line*
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/inline_styles.term.svg");
+}
+
+#[test]
+fn links() {
+    /// <https://example.com/literal>
+    ///
+    /// [with name](https://example.com/with%20name)
+    ///
+    /// ![image](https://example.com/image)
+    ///
+    /// [referencing][reference]
+    ///
+    /// [reference]: https://example.com/reference
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/links.term.svg");
+}
+
+#[test]
+fn html() {
+    /// <html>
+    ///     <is>
+    ///         <used>
+    ///     </verbatim>
+    /// </html>
+    ///
+    /// <inline>html</as-well>
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/html.term.svg");
+}
+
+#[test]
+fn blocks() {
+    /// ```rust
+    /// 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)
+    ///
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/blocks.term.svg");
+}
+
+#[test]
+fn lists() {
+    /// Lists:
+    ///
+    /// - unordered
+    ///   - bullet
+    ///     - lists
+    /// - with multiple
+    ///   - levels
+    ///
+    /// 0. numeric lists
+    /// 1. only care
+    ///    1. about the initial number
+    /// 2. 5. and count from there
+    ///    7. anything goes
+    /// 3. though they need an empty line
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/lists.term.svg");
+}
+
+#[test]
+fn paragraphs() {
+    /// 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.
+    ///
+    #[doc = "You can also use trailing spaces for hard breaks,  \nbut this is not really recommended."]
+    #[derive(Parser)]
+    struct Command;
+
+    assert_help!(Command, "snapshots/paragraphs.term.svg");
+}
diff --git a/tests/derive/snapshots/blocks.term.svg b/tests/derive/snapshots/blocks.term.svg
new file mode 100644
index 00000000000..22dda094153
--- /dev/null
+++ b/tests/derive/snapshots/blocks.term.svg
@@ -0,0 +1,87 @@
+<svg width="740px" height="578px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .italic { font-style: italic; }
+    .underline { text-decoration-line: underline; }
+    .strikethrough { text-decoration-line: line-through; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan>  </tspan><tspan class="bold">This is a *fenced* code block.</tspan>
+</tspan>
+    <tspan x="10px" y="46px"><tspan>  </tspan>
+</tspan>
+    <tspan x="10px" y="64px"><tspan>  </tspan><tspan class="bold">There is not much going on in terms of **styling**.</tspan>
+</tspan>
+    <tspan x="10px" y="82px">
+</tspan>
+    <tspan x="10px" y="100px"><tspan>---</tspan>
+</tspan>
+    <tspan x="10px" y="118px">
+</tspan>
+    <tspan x="10px" y="136px"><tspan>  </tspan><tspan class="bold">Code blocks can also be initiated through</tspan>
+</tspan>
+    <tspan x="10px" y="154px"><tspan>  </tspan><tspan class="bold">Indentation.</tspan>
+</tspan>
+    <tspan x="10px" y="172px">
+</tspan>
+    <tspan x="10px" y="190px"><tspan>| This is a block quote. </tspan><tspan class="bold">Regular </tspan><tspan class="strikethrough bold">styling </tspan><tspan class="strikethrough bold italic">should</tspan><tspan class="strikethrough bold"> work</tspan><tspan class="bold"> here.</tspan>
+</tspan>
+    <tspan x="10px" y="208px"><tspan>| </tspan>
+</tspan>
+    <tspan x="10px" y="226px"><tspan>| </tspan><tspan class="underline bold">even headings</tspan>
+</tspan>
+    <tspan x="10px" y="244px"><tspan>| </tspan>
+</tspan>
+    <tspan x="10px" y="262px"><tspan>| and regular paragraphs.</tspan>
+</tspan>
+    <tspan x="10px" y="280px"><tspan>| </tspan>
+</tspan>
+    <tspan x="10px" y="298px"><tspan>| - lists</tspan>
+</tspan>
+    <tspan x="10px" y="316px"><tspan>|   - are</tspan>
+</tspan>
+    <tspan x="10px" y="334px"><tspan>| </tspan>
+</tspan>
+    <tspan x="10px" y="352px"><tspan>| 1. also</tspan>
+</tspan>
+    <tspan x="10px" y="370px"><tspan>|   1. supported</tspan>
+</tspan>
+    <tspan x="10px" y="388px"><tspan>| </tspan>
+</tspan>
+    <tspan x="10px" y="406px"><tspan>| | nesting them</tspan>
+</tspan>
+    <tspan x="10px" y="424px"><tspan>| | </tspan>
+</tspan>
+    <tspan x="10px" y="442px"><tspan>| | | also works (not)</tspan>
+</tspan>
+    <tspan x="10px" y="460px">
+</tspan>
+    <tspan x="10px" y="478px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="496px">
+</tspan>
+    <tspan x="10px" y="514px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="532px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="550px"><tspan>          Print help (see a summary with '-h')</tspan>
+</tspan>
+    <tspan x="10px" y="568px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="272px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .italic { font-style: italic; }
+    .underline { text-decoration-line: underline; }
+    .strikethrough { text-decoration-line: line-through; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan class="underline bold">This is a header</tspan>
+</tspan>
+    <tspan x="10px" y="46px">
+</tspan>
+    <tspan x="10px" y="64px"><tspan class="underline bold">second level</tspan>
+</tspan>
+    <tspan x="10px" y="82px">
+</tspan>
+    <tspan x="10px" y="100px"><tspan class="underline bold">additional</tspan><tspan class="underline bold"> </tspan><tspan class="underline bold italic">styling on </tspan><tspan class="underline strikethrough bold italic">top</tspan><tspan class="underline bold italic"> of</tspan><tspan class="underline bold italic"> it</tspan>
+</tspan>
+    <tspan x="10px" y="118px">
+</tspan>
+    <tspan x="10px" y="136px"><tspan>regular paragraph</tspan>
+</tspan>
+    <tspan x="10px" y="154px">
+</tspan>
+    <tspan x="10px" y="172px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="190px">
+</tspan>
+    <tspan x="10px" y="208px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="226px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="244px"><tspan>          Print help</tspan>
+</tspan>
+    <tspan x="10px" y="262px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="290px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .underline { text-decoration-line: underline; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan>&lt;html&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="46px"><tspan>    &lt;is&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="64px"><tspan>        &lt;used&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="82px"><tspan>    &lt;/verbatim&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="100px"><tspan>&lt;/html&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="118px">
+</tspan>
+    <tspan x="10px" y="136px">
+</tspan>
+    <tspan x="10px" y="154px"><tspan>&lt;inline&gt;html&lt;/as-well&gt;</tspan>
+</tspan>
+    <tspan x="10px" y="172px">
+</tspan>
+    <tspan x="10px" y="190px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="208px">
+</tspan>
+    <tspan x="10px" y="226px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="244px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="262px"><tspan>          Print help</tspan>
+</tspan>
+    <tspan x="10px" y="280px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="200px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .italic { font-style: italic; }
+    .underline { text-decoration-line: underline; }
+    .strikethrough { text-decoration-line: line-through; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan class="italic">emphasis</tspan><tspan> </tspan><tspan class="bold">bold</tspan><tspan> </tspan><tspan class="strikethrough">strike through</tspan><tspan> </tspan><tspan class="bold">code</tspan>
+</tspan>
+    <tspan x="10px" y="46px">
+</tspan>
+    <tspan x="10px" y="64px"><tspan class="italic">all </tspan><tspan class="bold italic">of </tspan><tspan class="strikethrough bold italic">them combined</tspan><tspan class="strikethrough bold italic"> in</tspan><tspan class="bold italic"> one</tspan><tspan class="italic"> line</tspan>
+</tspan>
+    <tspan x="10px" y="82px">
+</tspan>
+    <tspan x="10px" y="100px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="118px">
+</tspan>
+    <tspan x="10px" y="136px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="154px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="172px"><tspan>          Print help (see a summary with '-h')</tspan>
+</tspan>
+    <tspan x="10px" y="190px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="272px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .underline { text-decoration-line: underline; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan class="underline">https://example.com/literal</tspan>
+</tspan>
+    <tspan x="10px" y="46px">
+</tspan>
+    <tspan x="10px" y="64px"><tspan class="underline">with name</tspan>
+</tspan>
+    <tspan x="10px" y="82px">
+</tspan>
+    <tspan x="10px" y="100px"><tspan>image</tspan>
+</tspan>
+    <tspan x="10px" y="118px">
+</tspan>
+    <tspan x="10px" y="136px"><tspan class="underline">referencing</tspan>
+</tspan>
+    <tspan x="10px" y="154px">
+</tspan>
+    <tspan x="10px" y="172px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="190px">
+</tspan>
+    <tspan x="10px" y="208px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="226px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="244px"><tspan>          Print help (see a summary with '-h')</tspan>
+</tspan>
+    <tspan x="10px" y="262px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="416px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .underline { text-decoration-line: underline; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan>Lists:</tspan>
+</tspan>
+    <tspan x="10px" y="46px">
+</tspan>
+    <tspan x="10px" y="64px"><tspan>- unordered</tspan>
+</tspan>
+    <tspan x="10px" y="82px"><tspan>  - bullet</tspan>
+</tspan>
+    <tspan x="10px" y="100px"><tspan>    - lists</tspan>
+</tspan>
+    <tspan x="10px" y="118px"><tspan>- with multiple</tspan>
+</tspan>
+    <tspan x="10px" y="136px"><tspan>  - levels</tspan>
+</tspan>
+    <tspan x="10px" y="154px">
+</tspan>
+    <tspan x="10px" y="172px"><tspan>0. numeric lists</tspan>
+</tspan>
+    <tspan x="10px" y="190px"><tspan>1. only care</tspan>
+</tspan>
+    <tspan x="10px" y="208px"><tspan>  1. about the initial number</tspan>
+</tspan>
+    <tspan x="10px" y="226px"><tspan>2. </tspan>
+</tspan>
+    <tspan x="10px" y="244px"><tspan>  5. and count from there</tspan>
+</tspan>
+    <tspan x="10px" y="262px"><tspan>  6. anything goes</tspan>
+</tspan>
+    <tspan x="10px" y="280px"><tspan>3. though they need an empty line</tspan>
+</tspan>
+    <tspan x="10px" y="298px">
+</tspan>
+    <tspan x="10px" y="316px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="334px">
+</tspan>
+    <tspan x="10px" y="352px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="370px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="388px"><tspan>          Print help (see a summary with '-h')</tspan>
+</tspan>
+    <tspan x="10px" y="406px">
+</tspan>
+  </text>
+
+</svg>
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 @@
+<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .fg { fill: #AAAAAA }
+    .bg { background: #000000 }
+    .container {
+      padding: 0 10px;
+      line-height: 18px;
+    }
+    .bold { font-weight: bold; }
+    .underline { text-decoration-line: underline; }
+    tspan {
+      font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+      white-space: pre;
+      line-height: 18px;
+    }
+  </style>
+
+  <rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
+
+  <text xml:space="preserve" class="container fg">
+    <tspan x="10px" y="28px"><tspan>Paragraphs are separated by empty lines. All lines will be joined onto one.</tspan>
+</tspan>
+    <tspan x="10px" y="46px">
+</tspan>
+    <tspan x="10px" y="64px"><tspan>The first paragraph is used as short help by clap.</tspan>
+</tspan>
+    <tspan x="10px" y="82px"><tspan>backslashes can be used to insert hard line breaks.</tspan>
+</tspan>
+    <tspan x="10px" y="100px">
+</tspan>
+    <tspan x="10px" y="118px"><tspan>| these | can   |</tspan>
+</tspan>
+    <tspan x="10px" y="136px"><tspan>| ----- | ----- |</tspan>
+</tspan>
+    <tspan x="10px" y="154px"><tspan>| be    | used  |</tspan>
+</tspan>
+    <tspan x="10px" y="172px"><tspan>| for   | tables|</tspan>
+</tspan>
+    <tspan x="10px" y="190px">
+</tspan>
+    <tspan x="10px" y="208px"><tspan>Because tables are not yet supported.</tspan>
+</tspan>
+    <tspan x="10px" y="226px">
+</tspan>
+    <tspan x="10px" y="244px"><tspan>You can also use trailing spaces for hard breaks,</tspan>
+</tspan>
+    <tspan x="10px" y="262px"><tspan>but this is not really recommended.</tspan>
+</tspan>
+    <tspan x="10px" y="280px">
+</tspan>
+    <tspan x="10px" y="298px"><tspan class="underline bold">Usage:</tspan><tspan> </tspan><tspan class="bold">clap</tspan>
+</tspan>
+    <tspan x="10px" y="316px">
+</tspan>
+    <tspan x="10px" y="334px"><tspan class="underline bold">Options:</tspan>
+</tspan>
+    <tspan x="10px" y="352px"><tspan>  </tspan><tspan class="bold">-h</tspan><tspan>, </tspan><tspan class="bold">--help</tspan>
+</tspan>
+    <tspan x="10px" y="370px"><tspan>          Print help (see a summary with '-h')</tspan>
+</tspan>
+    <tspan x="10px" y="388px">
+</tspan>
+  </text>
+
+</svg>