1#![doc = include_str!("../README.md")]
2
3use core::fmt::{self, Write as _};
4
5use clap_builder::{Command, CommandFactory};
6use stackstack::Stack;
7
8pub fn markdown(cmd: Command) -> String {
12 let mut buf = String::new();
13 _markdown(&mut buf, Stack::new(), &cmd).expect("fmt::write returned an error");
14 buf
15}
16
17pub fn markdown_for<T: CommandFactory>() -> String {
21 markdown(T::command())
22}
23
24fn _markdown(buf: &mut String, path: Stack<&str>, cmd: &Command) -> fmt::Result {
25 if !path.is_empty() {
26 buf.push('\n')
27 }
28 let path = path.pushed(cmd.get_name());
29 for _ in 0..path.len() {
30 buf.push('#')
31 }
32 for component in &path {
33 buf.write_fmt(format_args!(" `{component}`"))?;
34 }
35
36 let mut cmd = cmd
37 .clone()
38 .disable_help_subcommand(true)
39 .disable_help_flag(true);
40 cmd.set_bin_name(join(&path)?); fmt::write(
42 buf,
43 format_args!(
44 "\n```text\n{}\n```",
45 cmd.render_long_help().to_string().trim()
46 ),
47 )?;
48 for sub in cmd.get_subcommands() {
49 _markdown(buf, path, sub)?
50 }
51 Ok(())
52}
53
54fn join<T: fmt::Display>(it: impl IntoIterator<Item = T>) -> Result<String, fmt::Error> {
55 let mut it = it.into_iter().peekable();
56 let mut s = String::new();
57 while let Some(comp) = it.next() {
58 s.write_fmt(format_args!("{comp}"))?;
59 if it.peek().is_some() {
60 s.push(' ');
61 }
62 }
63 Ok(s)
64}
65
66#[cfg(test)]
67mod tests {
68 include!("test-snippet.rs");
69
70 use clap::Parser;
71 use expect_test::{expect, expect_file};
72 use indoc::formatdoc;
73
74 use super::*;
75
76 #[test]
77 fn readme() {
78 let crate_name = env!("CARGO_PKG_NAME");
79 let test_snippet = include_str!("test-snippet.rs").trim();
80 let markdown = markdown(Cargo::command());
81 let readme = formatdoc! {
82 "
83 # `{crate_name}`
84 Create markdown descriptions for [`clap::Command`]s.
85
86 So given the following rust code:
87 ```rust
88 {test_snippet}
89 ```
90
91 You get the markdown that follows,
92 with subcommands handled as you'd expect.
93 ---
94 {markdown}
95 "
96 };
97 expect_file!["../README.md"].assert_eq(&readme);
98 }
99
100 #[derive(Parser)]
102 #[command(name = "simple")]
103 struct Simple {
104 #[arg(short, long)]
106 flag: bool,
107
108 pos: String,
110 #[arg(short, long)]
112 switch: String,
113
114 opt_pos: Option<String>,
116 #[arg(short, long)]
118 opt_switch: Option<String>,
119
120 #[arg(short, long, default_value = "default")]
122 default_switch: String,
123 }
124
125 #[test]
126 fn simple() {
127 expect![[r#"
128 # `simple`
129 ```text
130 This is a top-level description
131
132 Usage: simple [OPTIONS] --switch <SWITCH> <POS> [OPT_POS]
133
134 Arguments:
135 <POS>
136 This is a mandatory positional argument
137
138 [OPT_POS]
139 This is an optional positional argument
140
141 Options:
142 -f, --flag
143 This is a flag
144
145 -s, --switch <SWITCH>
146 This is a mandatory switched argument
147
148 -o, --opt-switch <OPT_SWITCH>
149 This is an optional switched argument
150
151 -d, --default-switch <DEFAULT_SWITCH>
152 This is a switched argument with a default
153
154 [default: default]
155 ```"#]]
156 .assert_eq(&markdown(Simple::command()));
157 }
158}