1#[doc(hidden)]
11mod test_readme {
12 #![doc = include_str!("../README.md")]
13}
14
15mod utils;
16
17use std::fmt::{self, Write};
18
19use clap::builder::PossibleValue;
20
21use utils::pluralize;
22
23#[non_exhaustive]
31pub struct MarkdownOptions {
32 title: Option<String>,
33 show_footer: bool,
34 show_table_of_contents: bool,
35 show_aliases: bool,
36}
37
38impl MarkdownOptions {
39 pub fn new() -> Self {
41 return Self {
42 title: None,
43 show_footer: true,
44 show_table_of_contents: true,
45 show_aliases: true,
46 };
47 }
48
49 pub fn title(mut self, title: String) -> Self {
51 self.title = Some(title);
52
53 return self;
54 }
55
56 pub fn show_footer(mut self, show: bool) -> Self {
58 self.show_footer = show;
59
60 return self;
61 }
62
63 pub fn show_table_of_contents(mut self, show: bool) -> Self {
65 self.show_table_of_contents = show;
66
67 return self;
68 }
69
70 pub fn show_aliases(mut self, show: bool) -> Self {
72 self.show_aliases = show;
73
74 return self;
75 }
76}
77
78impl Default for MarkdownOptions {
79 fn default() -> Self {
80 return Self::new();
81 }
82}
83
84pub fn help_markdown<C: clap::CommandFactory>() -> String {
90 let command = C::command();
91
92 help_markdown_command(&command)
93}
94
95pub fn help_markdown_custom<C: clap::CommandFactory>(
97 options: &MarkdownOptions,
98) -> String {
99 let command = C::command();
100
101 return help_markdown_command_custom(&command, options);
102}
103
104pub fn help_markdown_command(command: &clap::Command) -> String {
106 return help_markdown_command_custom(command, &Default::default());
107}
108
109pub fn help_markdown_command_custom(
111 command: &clap::Command,
112 options: &MarkdownOptions,
113) -> String {
114 let mut buffer = String::with_capacity(100);
115
116 write_help_markdown(&mut buffer, &command, options);
117
118 buffer
119}
120
121pub fn print_help_markdown<C: clap::CommandFactory>() {
129 let command = C::command();
130
131 let mut buffer = String::with_capacity(100);
132
133 write_help_markdown(&mut buffer, &command, &Default::default());
134
135 println!("{}", buffer);
136}
137
138fn write_help_markdown(
139 buffer: &mut String,
140 command: &clap::Command,
141 options: &MarkdownOptions,
142) {
143 let title_name = get_canonical_name(command);
148
149 let title = match options.title {
150 Some(ref title) => title.to_owned(),
151 None => format!("Command-Line Help for `{title_name}`"),
152 };
153 writeln!(buffer, "# {title}\n",).unwrap();
154
155 writeln!(
156 buffer,
157 "This document contains the help content for the `{}` command-line program.\n",
158 title_name
159 ).unwrap();
160
161 if options.show_table_of_contents {
170 writeln!(buffer, "**Command Overview:**\n").unwrap();
171
172 build_table_of_contents_markdown(buffer, Vec::new(), command, 0)
173 .unwrap();
174
175 write!(buffer, "\n").unwrap();
176 }
177
178 build_command_markdown(buffer, Vec::new(), command, 0, options).unwrap();
183
184 if options.show_footer {
188 write!(buffer, r#"<hr/>
189
190<small><i>
191 This document was generated automatically by
192 <a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
193</i></small>
194"#).unwrap();
195 }
196}
197
198fn build_table_of_contents_markdown(
199 buffer: &mut String,
200 parent_command_path: Vec<String>,
202 command: &clap::Command,
203 depth: usize,
204) -> std::fmt::Result {
205 if command.is_hide_set() {
208 return Ok(());
209 }
210
211 let title_name = get_canonical_name(command);
212
213 let command_path = {
215 let mut command_path = parent_command_path;
216 command_path.push(title_name);
217 command_path
218 };
219
220 writeln!(
221 buffer,
222 "* [`{}`↴](#{})",
223 command_path.join(" "),
224 command_path.join("-"),
225 )?;
226
227 for subcommand in command.get_subcommands() {
232 build_table_of_contents_markdown(
233 buffer,
234 command_path.clone(),
235 subcommand,
236 depth + 1,
237 )?;
238 }
239
240 Ok(())
241}
242
243fn build_command_markdown(
289 buffer: &mut String,
290 parent_command_path: Vec<String>,
292 command: &clap::Command,
293 depth: usize,
294 options: &MarkdownOptions,
295) -> std::fmt::Result {
296 if command.is_hide_set() {
299 return Ok(());
300 }
301
302 let title_name = get_canonical_name(command);
303
304 let command_path = {
306 let mut command_path = parent_command_path.clone();
307 command_path.push(title_name);
308 command_path
309 };
310
311 writeln!(buffer, "## `{}`\n", command_path.join(" "))?;
325
326 if let Some(long_about) = command.get_long_about() {
327 writeln!(buffer, "{}\n", long_about)?;
328 } else if let Some(about) = command.get_about() {
329 writeln!(buffer, "{}\n", about)?;
330 }
331
332 if let Some(help) = command.get_before_long_help() {
333 writeln!(buffer, "{}\n", help)?;
334 } else if let Some(help) = command.get_before_help() {
335 writeln!(buffer, "{}\n", help)?;
336 }
337
338 writeln!(
339 buffer,
340 "**Usage:** `{}{}`\n",
341 if parent_command_path.is_empty() {
342 String::new()
343 } else {
344 let mut s = parent_command_path.join(" ");
345 s.push_str(" ");
346 s
347 },
348 command
349 .clone()
350 .render_usage()
351 .to_string()
352 .replace("Usage: ", "")
353 )?;
354
355 if options.show_aliases {
356 let aliases = command.get_visible_aliases().collect::<Vec<&str>>();
357 if let Some(aliases_str) = get_alias_string(&aliases) {
358 writeln!(
359 buffer,
360 "**{}:** {aliases_str}\n",
361 pluralize(aliases.len(), "Command Alias", "Command Aliases")
362 )?;
363 }
364 }
365
366 if let Some(help) = command.get_after_long_help() {
367 writeln!(buffer, "{}\n", help)?;
368 } else if let Some(help) = command.get_after_help() {
369 writeln!(buffer, "{}\n", help)?;
370 }
371
372 if command.get_subcommands().next().is_some() {
377 writeln!(buffer, "###### **Subcommands:**\n")?;
378
379 for subcommand in command.get_subcommands() {
380 if subcommand.is_hide_set() {
381 continue;
382 }
383
384 let title_name = get_canonical_name(subcommand);
385
386 let about = match subcommand.get_about() {
387 Some(about) => about.to_string(),
388 None => String::new(),
389 };
390
391 writeln!(buffer, "* `{title_name}` — {about}",)?;
392 }
393
394 write!(buffer, "\n")?;
395 }
396
397 if command.get_positionals().next().is_some() {
402 writeln!(buffer, "###### **Arguments:**\n")?;
403
404 for pos_arg in command.get_positionals() {
405 write_arg_markdown(buffer, pos_arg)?;
406 }
407
408 write!(buffer, "\n")?;
409 }
410
411 let non_pos: Vec<_> = command
416 .get_arguments()
417 .filter(|arg| !arg.is_positional() && !arg.is_hide_set())
418 .collect();
419
420 if !non_pos.is_empty() {
421 writeln!(buffer, "###### **Options:**\n")?;
422
423 for arg in non_pos {
424 write_arg_markdown(buffer, arg)?;
425 }
426
427 write!(buffer, "\n")?;
428 }
429
430 write!(buffer, "\n\n")?;
437
438 for subcommand in command.get_subcommands() {
439 build_command_markdown(
440 buffer,
441 command_path.clone(),
442 subcommand,
443 depth + 1,
444 options,
445 )?;
446 }
447
448 Ok(())
449}
450
451fn write_arg_markdown(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
452 write!(buffer, "* ")?;
454
455 let value_name: String = match arg.get_value_names() {
456 Some([name, ..]) => name.as_str().to_owned(),
458 Some([]) => unreachable!(
459 "clap Arg::get_value_names() returned Some(..) of empty list"
460 ),
461 None => arg.get_id().to_string().to_ascii_uppercase(),
462 };
463
464 match (arg.get_short(), arg.get_long()) {
465 (Some(short), Some(long)) => {
466 if arg.get_action().takes_values() {
467 write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
468 } else {
469 write!(buffer, "`-{short}`, `--{long}`")?
470 }
471 },
472 (Some(short), None) => {
473 if arg.get_action().takes_values() {
474 write!(buffer, "`-{short} <{value_name}>`")?
475 } else {
476 write!(buffer, "`-{short}`")?
477 }
478 },
479 (None, Some(long)) => {
480 if arg.get_action().takes_values() {
481 write!(buffer, "`--{} <{value_name}>`", long)?
482 } else {
483 write!(buffer, "`--{}`", long)?
484 }
485 },
486 (None, None) => {
487 debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
488
489 write!(buffer, "`<{value_name}>`",)?;
490 },
491 }
492
493 if let Some(aliases) = arg.get_visible_aliases().as_deref() {
494 if let Some(aliases_str) = get_alias_string(aliases) {
495 write!(
496 buffer,
497 " [{}: {aliases_str}]",
498 pluralize(aliases.len(), "alias", "aliases")
499 )?;
500 }
501 }
502
503 if let Some(help) = arg.get_long_help() {
504 buffer.push_str(&indent(&help.to_string(), " — ", " "))
506 } else if let Some(short_help) = arg.get_help() {
507 writeln!(buffer, " — {short_help}")?;
508 } else {
509 writeln!(buffer)?;
510 }
511
512 if !arg.get_default_values().is_empty() {
517 let default_values: String = arg
518 .get_default_values()
519 .iter()
520 .map(|value| format!("`{}`", value.to_string_lossy()))
521 .collect::<Vec<String>>()
522 .join(", ");
523
524 if arg.get_default_values().len() > 1 {
525 writeln!(buffer, "\n Default values: {default_values}")?;
527 } else {
528 writeln!(buffer, "\n Default value: {default_values}")?;
530 }
531 }
532
533 let possible_values: Vec<PossibleValue> = arg
538 .get_possible_values()
539 .into_iter()
540 .filter(|pv| !pv.is_hide_set())
541 .collect();
542
543 if !possible_values.is_empty()
546 && !matches!(arg.get_action(), clap::ArgAction::SetTrue)
547 {
548 let any_have_help: bool =
549 possible_values.iter().any(|pv| pv.get_help().is_some());
550
551 if any_have_help {
552 let text: String = possible_values
564 .iter()
565 .map(|pv| match pv.get_help() {
566 Some(help) => {
567 format!(" - `{}`:\n {}\n", pv.get_name(), help)
568 },
569 None => format!(" - `{}`\n", pv.get_name()),
570 })
571 .collect::<Vec<String>>()
572 .join("");
573
574 writeln!(buffer, "\n Possible values:\n{text}")?;
575 } else {
576 let text: String = possible_values
579 .iter()
580 .map(|pv| format!("`{}`", pv.get_name()))
582 .collect::<Vec<String>>()
583 .join(", ");
584
585 writeln!(buffer, "\n Possible values: {text}\n")?;
586 }
587 }
588
589 Ok(())
590}
591
592fn get_canonical_name(command: &clap::Command) -> String {
601 command
602 .get_display_name()
603 .or_else(|| command.get_bin_name())
604 .map(|name| name.to_owned())
605 .unwrap_or_else(|| command.get_name().to_owned())
606}
607
608fn indent(s: &str, first: &str, rest: &str) -> String {
610 if s.is_empty() {
611 return "\n".to_string();
614 }
615 let mut result = String::new();
616 let mut first_line = true;
617
618 for line in s.lines() {
619 if !line.is_empty() {
620 result.push_str(if first_line { first } else { rest });
621 result.push_str(line);
622 first_line = false;
623 }
624 result.push('\n');
625 }
626 result
627}
628
629fn get_alias_string(aliases: &[&str]) -> Option<String> {
630 if aliases.is_empty() {
631 return None;
632 }
633
634 Some(format!(
635 "{}",
636 aliases
637 .iter()
638 .map(|alias| format!("`{alias}`"))
639 .collect::<Vec<_>>()
640 .join(", ")
641 ))
642}
643
644#[cfg(test)]
645mod test {
646 use pretty_assertions::assert_eq;
647
648 #[test]
649 fn test_indent() {
650 use super::indent;
651 assert_eq!(
652 &indent("Header\n\nMore info", "___", "~~~~"),
653 "___Header\n\n~~~~More info\n"
654 );
655 assert_eq!(
656 &indent("Header\n\nMore info\n", "___", "~~~~"),
657 &indent("Header\n\nMore info", "___", "~~~~"),
658 );
659 assert_eq!(&indent("", "___", "~~~~"), "\n");
660 assert_eq!(&indent("\n", "___", "~~~~"), "\n");
661 }
662}