clap_markdown_dfir/
lib.rs

1//! Autogenerate Markdown documentation for clap command-line tools
2//!
3//! See [**Examples**][Examples] for examples of the content `clap-markdown`
4//! generates.
5//!
6//! [Examples]: https://github.com/ConnorGray/clap-markdown#Examples
7//!
8
9// Ensure that doc tests in the README.md file get run.
10#[allow(clippy::mixed_attributes_style)]
11#[doc(hidden)]
12mod test_readme {
13    #![doc = include_str!("../README.md")]
14}
15
16use std::{
17    default,
18    fmt::{self, Write},
19};
20
21use clap::builder::PossibleValue;
22
23/// Format the help information for `command` as Markdown.
24pub fn help_markdown<C: clap::CommandFactory>() -> String {
25    let command = C::command();
26
27    help_markdown_command(&command)
28}
29
30/// Format the help information for `command` as Markdown, with custom options.
31pub fn custom_help_markdown<C: clap::CommandFactory>(
32    options: MarkdownOptions,
33) -> String {
34    let command = C::command();
35
36    let mut buffer = String::with_capacity(100);
37
38    write_help_markdown(&mut buffer, &command, options);
39
40    buffer
41}
42
43/// Format the help information for `command` as Markdown.
44pub fn help_markdown_command(command: &clap::Command) -> String {
45    let mut buffer = String::with_capacity(100);
46
47    write_help_markdown(&mut buffer, command, default::Default::default());
48
49    buffer
50}
51
52//======================================
53// Markdown
54//======================================
55
56/// Format the help information for `command` as Markdown and print it.
57///
58/// Output is printed to the standard output, using [`println!`].
59pub fn print_help_markdown<C: clap::CommandFactory>(options: MarkdownOptions) {
60    let command = C::command();
61
62    let mut buffer = String::with_capacity(100);
63
64    write_help_markdown(&mut buffer, &command, options);
65
66    println!("{}", buffer);
67}
68
69#[derive(Default)]
70pub struct MarkdownOptions {
71    pub title: Option<String>,
72    pub hide_footer: bool,
73    pub disable_toc: bool,
74}
75
76fn write_help_markdown(
77    buffer: &mut String,
78    command: &clap::Command,
79    options: MarkdownOptions,
80) {
81    //----------------------------------
82    // Write the document title
83    //----------------------------------
84
85    let title_name = get_canonical_name(command);
86
87    let title = match options.title {
88        Some(ref title) => title.to_owned(),
89        None => format!("Command-Line Help for `{title_name}`"),
90    };
91    writeln!(buffer, "# {title}\n",).unwrap();
92
93    writeln!(
94        buffer,
95        "This document contains the help content for the `{}` command-line program.\n",
96        title_name
97    ).unwrap();
98
99    //----------------------------------
100    // Write the table of contents
101    //----------------------------------
102
103    // writeln!(buffer, r#"<div style="background: light-gray"><ul>"#).unwrap();
104    // build_table_of_contents_html(buffer, Vec::new(), command, 0).unwrap();
105    // writeln!(buffer, "</ul></div>").unwrap();
106
107    if ! options.disable_toc {
108        writeln!(buffer, "**Command Overview:**\n").unwrap();
109
110        build_table_of_contents_markdown(buffer, Vec::new(), command, 0)
111            .unwrap();
112
113        writeln!(buffer).unwrap();
114    }
115
116    //----------------------------------------
117    // Write the commands/subcommands sections
118    //----------------------------------------
119
120    build_command_markdown(buffer, Vec::new(), command, 0).unwrap();
121
122    //-----------------
123    // Write the footer
124    //-----------------
125    if !options.hide_footer {
126        write!(buffer, r#"<hr/>
127
128<small><i>
129    This document was generated automatically by
130    <a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
131</i></small>
132"#).unwrap();
133    }
134}
135
136#[allow(clippy::only_used_in_recursion)]
137fn build_table_of_contents_markdown(
138    buffer: &mut String,
139    // Parent commands of `command`.
140    parent_command_path: Vec<String>,
141    command: &clap::Command,
142    depth: usize,
143) -> std::fmt::Result {
144    // Don't document commands marked with `clap(hide = true)` (which includes
145    // `print-all-help`).
146    if command.is_hide_set() {
147        return Ok(());
148    }
149
150    let title_name = get_canonical_name(command);
151
152    // Append the name of `command` to `command_path`.
153    let command_path = {
154        let mut command_path = parent_command_path;
155        command_path.push(title_name);
156        command_path
157    };
158
159    writeln!(
160        buffer,
161        "* [`{}`↴](#{})",
162        command_path.join(" "),
163        command_path.join("-"),
164    )?;
165
166    //----------------------------------
167    // Recurse to write subcommands
168    //----------------------------------
169
170    for subcommand in command.get_subcommands() {
171        build_table_of_contents_markdown(
172            buffer,
173            command_path.clone(),
174            subcommand,
175            depth + 1,
176        )?;
177    }
178
179    Ok(())
180}
181
182/*
183fn build_table_of_contents_html(
184    buffer: &mut String,
185    // Parent commands of `command`.
186    parent_command_path: Vec<String>,
187    command: &clap::Command,
188    depth: usize,
189) -> std::fmt::Result {
190    // Don't document commands marked with `clap(hide = true)` (which includes
191    // `print-all-help`).
192    if command.is_hide_set() {
193        return Ok(());
194    }
195
196    // Append the name of `command` to `command_path`.
197    let command_path = {
198        let mut command_path = parent_command_path;
199        command_path.push(command.get_name().to_owned());
200        command_path
201    };
202
203    writeln!(
204        buffer,
205        "<li><a href=\"#{}\"><code>{}</code>↴</a></li>",
206        command_path.join("-"),
207        command_path.join(" ")
208    )?;
209
210    //----------------------------------
211    // Recurse to write subcommands
212    //----------------------------------
213
214    for subcommand in command.get_subcommands() {
215        build_table_of_contents_html(
216            buffer,
217            command_path.clone(),
218            subcommand,
219            depth + 1,
220        )?;
221    }
222
223    Ok(())
224}
225*/
226
227#[allow(clippy::only_used_in_recursion)]
228fn build_command_markdown(
229    buffer: &mut String,
230    // Parent commands of `command`.
231    parent_command_path: Vec<String>,
232    command: &clap::Command,
233    depth: usize,
234) -> std::fmt::Result {
235    // Don't document commands marked with `clap(hide = true)` (which includes
236    // `print-all-help`).
237    if command.is_hide_set() {
238        return Ok(());
239    }
240
241    let title_name = get_canonical_name(command);
242
243    // Append the name of `command` to `command_path`.
244    let command_path = {
245        let mut command_path = parent_command_path.clone();
246        command_path.push(title_name);
247        command_path
248    };
249
250    //----------------------------------
251    // Write the markdown heading
252    //----------------------------------
253
254    // TODO: `depth` is now unused. Remove if no other use for it appears.
255    /*
256    if depth >= 6 {
257        panic!(
258            "command path nesting depth is deeper than maximum markdown header depth: `{}`",
259            command_path.join(" ")
260        )
261    }
262    */
263
264    writeln!(
265        buffer,
266        "## `{}`\n",
267        command_path.join(" "),
268    )?;
269
270    if let Some(long_about) = command.get_long_about() {
271        writeln!(buffer, "{}\n", long_about)?;
272    } else if let Some(about) = command.get_about() {
273        writeln!(buffer, "{}\n", about)?;
274    }
275
276    if let Some(help) = command.get_before_long_help() {
277        writeln!(buffer, "{}\n", help)?;
278    } else if let Some(help) = command.get_before_help() {
279        writeln!(buffer, "{}\n", help)?;
280    }
281
282    writeln!(
283        buffer,
284        "**Usage:** `{}{}`\n",
285        if parent_command_path.is_empty() {
286            String::new()
287        } else {
288            let mut s = parent_command_path.join(" ");
289            s.push(' ');
290            s
291        },
292        command
293            .clone()
294            .render_usage()
295            .to_string()
296            .replace("Usage: ", "")
297    )?;
298
299    if let Some(help) = command.get_after_long_help() {
300        writeln!(buffer, "{}\n", help)?;
301    } else if let Some(help) = command.get_after_help() {
302        writeln!(buffer, "{}\n", help)?;
303    }
304
305    //----------------------------------
306    // Subcommands
307    //----------------------------------
308
309    if command.get_subcommands().next().is_some() {
310        writeln!(buffer, "###### **Subcommands:**\n")?;
311
312        for subcommand in command.get_subcommands() {
313            if subcommand.is_hide_set() {
314                continue;
315            }
316
317            let title_name = get_canonical_name(subcommand);
318
319            writeln!(
320                buffer,
321                "* `{}` — {}",
322                title_name,
323                match subcommand.get_about() {
324                    Some(about) => about.to_string(),
325                    None => String::new(),
326                }
327            )?;
328        }
329
330        writeln!(buffer)?;
331    }
332
333    //----------------------------------
334    // Arguments
335    //----------------------------------
336
337    if command.get_positionals().next().is_some() {
338        writeln!(buffer, "###### **Arguments:**\n")?;
339
340        for pos_arg in command.get_positionals() {
341            write_arg_markdown(buffer, pos_arg)?;
342        }
343
344        writeln!(buffer)?;
345    }
346
347    //----------------------------------
348    // Options
349    //----------------------------------
350
351    let non_pos: Vec<_> = command
352        .get_arguments()
353        .filter(|arg| !arg.is_positional())
354        .collect();
355
356    if !non_pos.is_empty() {
357        writeln!(buffer, "###### **Options:**\n")?;
358
359        for arg in non_pos {
360            write_arg_markdown(buffer, arg)?;
361        }
362
363        writeln!(buffer)?;
364    }
365
366    //----------------------------------
367    // Recurse to write subcommands
368    //----------------------------------
369
370    // Include extra space between commands. This is purely for the benefit of
371    // anyone reading the source .md file.
372    write!(buffer, "\n\n")?;
373
374    for subcommand in command.get_subcommands() {
375        build_command_markdown(
376            buffer,
377            command_path.clone(),
378            subcommand,
379            depth + 1,
380        )?;
381    }
382
383    Ok(())
384}
385
386fn write_arg_markdown(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
387    // Markdown list item
388    write!(buffer, "* ")?;
389
390    let value_name: String = match arg.get_value_names() {
391        // TODO: What if multiple names are provided?
392        Some([name, ..]) => name.as_str().to_owned(),
393        Some([]) => unreachable!(
394            "clap Arg::get_value_names() returned Some(..) of empty list"
395        ),
396        None => arg.get_id().to_string().to_ascii_uppercase(),
397    };
398
399    match (arg.get_short(), arg.get_long()) {
400        (Some(short), Some(long)) => {
401            if arg.get_action().takes_values() {
402                write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
403            } else {
404                write!(buffer, "`-{short}`, `--{long}`")?
405            }
406        },
407        (Some(short), None) => {
408            if arg.get_action().takes_values() {
409                write!(buffer, "`-{short} <{value_name}>`")?
410            } else {
411                write!(buffer, "`-{short}`")?
412            }
413        },
414        (None, Some(long)) => {
415            if arg.get_action().takes_values() {
416                write!(buffer, "`--{} <{value_name}>`", long)?
417            } else {
418                write!(buffer, "`--{}`", long)?
419            }
420        },
421        (None, None) => {
422            debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
423
424            write!(buffer, "`<{value_name}>`",)?;
425        },
426    }
427
428    if let Some(help) = arg.get_help() {
429        writeln!(buffer, " — {help}")?;
430    } else {
431        writeln!(buffer)?;
432    }
433
434    //--------------------
435    // Arg default values
436    //--------------------
437
438    if !arg.get_default_values().is_empty() {
439        let default_values: String = arg
440            .get_default_values()
441            .iter()
442            .map(|value| format!("`{}`", value.to_string_lossy()))
443            .collect::<Vec<String>>()
444            .join(", ");
445
446        if arg.get_default_values().len() > 1 {
447            // Plural
448            writeln!(buffer, "\n  Default values: {default_values}")?;
449        } else {
450            // Singular
451            writeln!(buffer, "\n  Default value: {default_values}")?;
452        }
453    }
454
455    //--------------------
456    // Arg possible values
457    //--------------------
458
459    let possible_values: Vec<PossibleValue> = arg
460        .get_possible_values()
461        .into_iter()
462        .filter(|pv| !pv.is_hide_set())
463        .collect();
464
465    if !possible_values.is_empty() {
466        let any_have_help: bool =
467            possible_values.iter().any(|pv| pv.get_help().is_some());
468
469        if any_have_help {
470            // If any of the possible values have help text, print them
471            // as a separate item in a bulleted list, and include the
472            // help text for those that have it. E.g.:
473            //
474            //     Possible values:
475            //     - `value1`:
476            //       The help text
477            //     - `value2`
478            //     - `value3`:
479            //       The help text
480
481            let text: String = possible_values
482                .iter()
483                .map(|pv| match pv.get_help() {
484                    Some(help) => {
485                        format!("  - `{}`:\n    {}\n", pv.get_name(), help)
486                    },
487                    None => format!("  - `{}`\n", pv.get_name()),
488                })
489                .collect::<Vec<String>>()
490                .join("");
491
492            writeln!(buffer, "\n  Possible values:\n{text}")?;
493        } else {
494            // If none of the possible values have any documentation, print
495            // them all inline on a single line.
496            let text: String = possible_values
497                .iter()
498                // TODO: Show PossibleValue::get_help(), and PossibleValue::get_name_and_aliases().
499                .map(|pv| format!("`{}`", pv.get_name()))
500                .collect::<Vec<String>>()
501                .join(", ");
502
503            writeln!(buffer, "\n  Possible values: {text}\n")?;
504        }
505    }
506
507    Ok(())
508}
509
510// This is a utility function to get the canonical name of a command. It's logic is
511// to get the display name if it exists, otherwise get the bin name if it exists, otherwise
512// get the package name.
513fn get_canonical_name(command: &clap::Command) -> String {
514    command
515        .get_display_name()
516        .or_else(|| command.get_bin_name())
517        .map(|name| name.to_owned())
518        .unwrap_or_else(|| command.get_name().to_owned())
519}