Skip to main content

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