Skip to main content

clap_documentation/
markdown.rs

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