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#[non_exhaustive]
16pub struct MarkdownOptions {
17 title: Option<String>,
18 show_table_of_contents: bool,
19 show_aliases: bool,
20}
21
22impl MarkdownOptions {
23 pub fn new() -> Self {
25 return Self {
26 title: None,
27 show_table_of_contents: true,
28 show_aliases: true,
29 };
30 }
31
32 pub fn title(mut self, title: String) -> Self {
34 self.title = Some(title);
35
36 return self;
37 }
38
39 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 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
60pub fn help_markdown<C: clap::CommandFactory>() -> String {
66 let command = C::command();
67
68 help_markdown_command(&command)
69}
70
71pub 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
80pub fn help_markdown_command(command: &clap::Command) -> String {
82 return help_markdown_command_custom(command, &Default::default());
83}
84
85pub 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
97pub 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 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 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 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_command_path: Vec<String>,
166 command: &clap::Command,
167 depth: usize,
168) -> std::fmt::Result {
169 if command.is_hide_set() {
172 return Ok(());
173 }
174
175 let title_name = get_canonical_name(command);
176
177 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 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
207fn build_command_markdown(
253 buffer: &mut String,
254 parent_command_path: Vec<String>,
256 command: &clap::Command,
257 depth: usize,
258 options: &MarkdownOptions,
259) -> std::fmt::Result {
260 if command.is_hide_set() {
263 return Ok(());
264 }
265
266 let title_name = get_canonical_name(command);
267
268 let command_path = {
270 let mut command_path = parent_command_path.clone();
271 command_path.push(title_name);
272 command_path
273 };
274
275 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 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 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 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 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 write!(buffer, "* ")?;
418
419 let value_name: String = match arg.get_value_names() {
420 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 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 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 writeln!(buffer, "\n Default values: {default_values}")?;
491 } else {
492 writeln!(buffer, "\n Default value: {default_values}")?;
494 }
495 }
496
497 let possible_values: Vec<PossibleValue> = arg
502 .get_possible_values()
503 .into_iter()
504 .filter(|pv| !pv.is_hide_set())
505 .collect();
506
507 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 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 let text: String = possible_values
543 .iter()
544 .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}