1use std::{ffi::OsStr, fmt, sync::Arc};
23
24use clap::{Arg, ArgAction, Command, CommandFactory};
25
26pub fn generate_markdown<T>() -> String
31where
32 T: CommandFactory,
33{
34 generate_markdown_for_command(T::command())
35}
36
37pub fn generate_markdown_for_command(command: Command) -> String {
39 MarkdownRenderer::default().render(command)
40}
41
42#[derive(Debug, Clone)]
44pub struct MarkdownOptions {
45 pub include_hidden: bool,
47 pub include_subcommands: bool,
49 pub include_toc: bool,
51 pub skip_parameter_details: bool,
53 pub include_html_anchors: bool,
55 pub include_usage: bool,
57 pub command_heading: CommandHeadingStyle,
59 pub summary: SummaryOptions,
61 pub parameter_heading: ParameterHeadingStyle,
63 pub parameter_content: ParameterContentStyle,
65}
66
67impl Default for MarkdownOptions {
68 fn default() -> Self {
69 Self {
70 include_hidden: false,
71 include_subcommands: true,
72 include_toc: true,
73 skip_parameter_details: false,
74 include_html_anchors: true,
75 include_usage: true,
76 command_heading: CommandHeadingStyle::Display,
77 summary: SummaryOptions::default(),
78 parameter_heading: ParameterHeadingStyle::Display,
79 parameter_content: ParameterContentStyle::Table,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct SummaryOptions {
87 pub enabled: bool,
89 pub value_style: SummaryValueStyle,
91 pub include_description: bool,
93 pub entry: SummaryEntryStyle,
95}
96
97impl Default for SummaryOptions {
98 fn default() -> Self {
99 Self {
100 enabled: true,
101 value_style: SummaryValueStyle::NamesAndValues,
102 include_description: true,
103 entry: SummaryEntryStyle::Default,
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct CommandInfo {
111 pub name: String,
113 pub path: Vec<String>,
115 pub display: String,
117 pub description: Option<String>,
119 pub heading_level: usize,
121}
122
123#[derive(Debug, Clone)]
125pub struct ParameterInfo {
126 pub anchor: String,
128 pub name: String,
130 pub display: String,
132 pub display_names: String,
134 pub description: Option<String>,
136 pub required: bool,
138 pub multiple: bool,
140 pub takes_value: bool,
142 pub value_names: Vec<String>,
144 pub default_values: Vec<String>,
146 pub env: Option<String>,
148 pub possible_values: Vec<String>,
150}
151
152#[derive(Clone)]
154pub enum CommandHeadingStyle {
155 Display,
157 None,
159 Custom(Arc<dyn Fn(&CommandInfo) -> String + Send + Sync + 'static>),
163}
164
165impl CommandHeadingStyle {
166 pub fn custom(formatter: impl Fn(&CommandInfo) -> String + Send + Sync + 'static) -> Self {
168 Self::Custom(Arc::new(formatter))
169 }
170
171 pub fn render(&self, command: &CommandInfo) -> String {
173 match self {
174 Self::Display => format!("`{}`", escape_markdown_text(&command.display)),
175 Self::None => String::new(),
176 Self::Custom(formatter) => formatter(command),
177 }
178 }
179}
180
181impl fmt::Debug for CommandHeadingStyle {
182 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::Display => formatter.write_str("Display"),
185 Self::None => formatter.write_str("None"),
186 Self::Custom(_) => formatter.write_str("Custom(<callback>)"),
187 }
188 }
189}
190
191#[derive(Clone)]
193pub enum SummaryEntryStyle {
194 Default,
196 Custom(Arc<dyn Fn(&ParameterInfo) -> String + Send + Sync + 'static>),
201}
202
203impl SummaryEntryStyle {
204 pub fn custom(formatter: impl Fn(&ParameterInfo) -> String + Send + Sync + 'static) -> Self {
206 Self::Custom(Arc::new(formatter))
207 }
208
209 pub fn render(&self, parameter: &ParameterInfo, options: &SummaryOptions, output: &mut String) {
211 match self {
212 Self::Default => render_default_summary_entry(parameter, options, output),
213 Self::Custom(formatter) => {
214 let entry = formatter(parameter);
215 if !entry.is_empty() {
216 output.push_str(&entry);
217 if !entry.ends_with('\n') {
218 output.push('\n');
219 }
220 }
221 }
222 }
223 }
224}
225
226impl fmt::Debug for SummaryEntryStyle {
227 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 Self::Default => formatter.write_str("Default"),
230 Self::Custom(_) => formatter.write_str("Custom(<callback>)"),
231 }
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum SummaryValueStyle {
238 NamesOnly,
240 NamesAndValues,
242}
243
244#[derive(Clone)]
246pub enum ParameterHeadingStyle {
247 Display,
249 Name,
251 Custom(Arc<dyn Fn(&ParameterInfo) -> String + Send + Sync + 'static>),
255}
256
257impl ParameterHeadingStyle {
258 pub fn custom(formatter: impl Fn(&ParameterInfo) -> String + Send + Sync + 'static) -> Self {
260 Self::Custom(Arc::new(formatter))
261 }
262
263 pub fn render(&self, parameter: &ParameterInfo) -> String {
265 match self {
266 Self::Display => {
267 format!("`{}`", escape_markdown_text(¶meter.display))
268 }
269 Self::Name => parameter.name.clone(),
270 Self::Custom(formatter) => formatter(parameter),
271 }
272 }
273}
274
275impl fmt::Debug for ParameterHeadingStyle {
276 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277 match self {
278 Self::Display => formatter.write_str("Display"),
279 Self::Name => formatter.write_str("Name"),
280 Self::Custom(_) => formatter.write_str("Custom(<callback>)"),
281 }
282 }
283}
284
285#[derive(Clone)]
287pub enum ParameterContentStyle {
288 Table,
290 Text,
292 Custom(Arc<dyn Fn(&ParameterInfo) -> String + Send + Sync + 'static>),
296}
297
298impl ParameterContentStyle {
299 pub fn custom(formatter: impl Fn(&ParameterInfo) -> String + Send + Sync + 'static) -> Self {
301 Self::Custom(Arc::new(formatter))
302 }
303
304 pub fn render(
306 &self,
307 parameter: &ParameterInfo,
308 options: &ParameterContentOptions,
309 output: &mut String,
310 ) {
311 match self {
312 Self::Table => render_parameter_table(parameter, options, output),
313 Self::Text => render_parameter_text(parameter, options, output),
314 Self::Custom(formatter) => {
315 push_custom_block(output, &formatter(parameter));
316 }
317 }
318 }
319}
320
321impl fmt::Debug for ParameterContentStyle {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 match self {
324 Self::Table => formatter.write_str("Table"),
325 Self::Text => formatter.write_str("Text"),
326 Self::Custom(_) => formatter.write_str("Custom(<callback>)"),
327 }
328 }
329}
330
331#[derive(Debug, Clone, Copy)]
332pub struct ParameterContentOptions {
333 pub include_usage: bool,
334}
335
336pub fn generate_markdown_for_command_with_options(
338 command: Command,
339 options: MarkdownOptions,
340) -> String {
341 MarkdownRenderer { options }.render(command)
342}
343
344#[derive(Debug, Default)]
345struct MarkdownRenderer {
346 options: MarkdownOptions,
347}
348
349impl MarkdownRenderer {
350 fn render(&self, mut command: Command) -> String {
351 command.build();
352
353 let mut output = String::new();
354 let command_path = vec![command.get_name().to_owned()];
355
356 self.render_command(&command, &command_path, 1, &mut output);
357 trim_trailing_blank_lines(&mut output);
358 output.push('\n');
359 output
360 }
361
362 fn render_command(
363 &self,
364 command: &Command,
365 command_path: &[String],
366 heading_level: usize,
367 output: &mut String,
368 ) {
369 let command_info = command_info(command, command_path, heading_level);
370 let command_heading = self.options.command_heading.render(&command_info);
371 if !command_heading.is_empty() {
372 push_heading(output, heading_level, &command_heading);
373 }
374
375 if let Some(description) = &command_info.description {
376 output.push_str(description);
377 output.push_str("\n\n");
378 }
379
380 let parameters = self.collect_parameters(command, command_path);
381 if self.options.include_toc && self.options.summary.enabled && !parameters.is_empty() {
382 push_heading(output, heading_level + 1, "Parameters");
383 for parameter in ¶meters {
384 self.render_summary_entry(parameter, output);
385 }
386 output.push('\n');
387 }
388
389 if !self.options.skip_parameter_details {
390 for parameter in ¶meters {
391 self.render_parameter(parameter, heading_level + 2, output);
392 }
393 }
394
395 let subcommands = self.visible_subcommands(command);
396 if self.options.include_subcommands && !subcommands.is_empty() {
397 push_heading(output, heading_level + 1, "Subcommands");
398 for subcommand in &subcommands {
399 let mut subcommand_path = command_path.to_vec();
400 subcommand_path.push(subcommand.get_name().to_owned());
401
402 let anchor = slugify(&subcommand_path.join("-"));
403 if self.options.include_html_anchors {
404 output.push_str(&format!("<a id=\"{}\"></a>\n", anchor));
405 }
406 output.push_str(&format!("- [`{}`](#{})", subcommand.get_name(), anchor));
407 if let Some(description) = command_description(subcommand)
408 && let Some(summary) = summary_line(&description)
409 {
410 output.push_str(&format!(": {}", summary));
411 }
412 output.push('\n');
413 }
414 output.push('\n');
415
416 for subcommand in subcommands {
417 let mut subcommand_path = command_path.to_vec();
418 subcommand_path.push(subcommand.get_name().to_owned());
419 self.render_command(subcommand, &subcommand_path, heading_level + 1, output);
420 }
421 }
422 }
423
424 fn render_parameter(
425 &self,
426 parameter: &ParameterInfo,
427 heading_level: usize,
428 output: &mut String,
429 ) {
430 if self.options.include_html_anchors {
431 output.push_str(&format!("<a id=\"{}\"></a>\n", parameter.anchor));
432 }
433 let heading = self.options.parameter_heading.render(parameter);
434 if !heading.is_empty() {
435 push_heading(output, heading_level, &heading);
436 }
437
438 if let Some(description) = ¶meter.description {
439 output.push_str(description);
440 output.push_str("\n\n");
441 }
442
443 self.options.parameter_content.render(
444 parameter,
445 &ParameterContentOptions {
446 include_usage: self.options.include_usage,
447 },
448 output,
449 );
450 }
451
452 fn render_summary_entry(&self, parameter: &ParameterInfo, output: &mut String) {
453 self.options
454 .summary
455 .entry
456 .render(parameter, &self.options.summary, output);
457 }
458
459 fn collect_parameters(&self, command: &Command, command_path: &[String]) -> Vec<ParameterInfo> {
460 command
461 .get_arguments()
462 .filter(|arg| !is_generated_action(arg))
463 .filter(|arg| self.options.include_hidden || !arg.is_hide_set())
464 .map(|arg| parameter_doc(arg, command_path))
465 .collect()
466 }
467
468 fn visible_subcommands<'a>(&self, command: &'a Command) -> Vec<&'a Command> {
469 if !self.options.include_subcommands {
470 return Vec::new();
471 }
472
473 command
474 .get_subcommands()
475 .filter(|subcommand| !is_generated_help_subcommand(subcommand))
476 .filter(|subcommand| self.options.include_hidden || !subcommand.is_hide_set())
477 .collect()
478 }
479}
480
481fn render_default_summary_entry(
482 parameter: &ParameterInfo,
483 options: &SummaryOptions,
484 output: &mut String,
485) {
486 let summary_display = match options.value_style {
487 SummaryValueStyle::NamesOnly => ¶meter.display_names,
488 SummaryValueStyle::NamesAndValues => ¶meter.display,
489 };
490 output.push_str(&format!(
491 "- [`{}`](#{})",
492 escape_markdown_text(summary_display),
493 parameter.anchor
494 ));
495
496 if options.include_description
497 && let Some(description) = ¶meter.description
498 && let Some(summary) = summary_line(description)
499 {
500 output.push_str(&format!(": {}", summary));
501 }
502
503 output.push('\n');
504}
505
506fn render_parameter_table(
507 parameter: &ParameterInfo,
508 options: &ParameterContentOptions,
509 output: &mut String,
510) {
511 output.push_str("| Field | Value |\n");
512 output.push_str("| --- | --- |\n");
513 if options.include_usage {
514 output.push_str(&format!(
515 "| Usage | `{}` |\n",
516 escape_table_cell(¶meter.display)
517 ));
518 }
519 output.push_str(&format!("| Required | {} |\n", yes_no(parameter.required)));
520 output.push_str(&format!(
521 "| Value | {} |\n",
522 if parameter.takes_value { "Yes" } else { "No" }
523 ));
524
525 if !parameter.value_names.is_empty() {
526 output.push_str(&format!(
527 "| Value name | {} |\n",
528 parameter
529 .value_names
530 .iter()
531 .map(|value| format!("`{}`", escape_table_cell(value)))
532 .collect::<Vec<_>>()
533 .join(", ")
534 ));
535 }
536
537 if parameter.multiple {
538 output.push_str("| Multiple | Yes |\n");
539 }
540
541 if !parameter.default_values.is_empty() {
542 output.push_str(&format!(
543 "| Default value | {} |\n",
544 parameter
545 .default_values
546 .iter()
547 .map(|value| format!("`{}`", escape_table_cell(value)))
548 .collect::<Vec<_>>()
549 .join(", ")
550 ));
551 }
552
553 if let Some(env) = ¶meter.env {
554 output.push_str(&format!("| Environment | `{}` |\n", escape_table_cell(env)));
555 }
556
557 if !parameter.possible_values.is_empty() {
558 output.push_str(&format!(
559 "| Possible values | {} |\n",
560 parameter
561 .possible_values
562 .iter()
563 .map(|value| format!("`{}`", escape_table_cell(value)))
564 .collect::<Vec<_>>()
565 .join(", ")
566 ));
567 }
568
569 output.push('\n');
570}
571
572fn render_parameter_text(
573 parameter: &ParameterInfo,
574 options: &ParameterContentOptions,
575 output: &mut String,
576) {
577 let mut parts = vec![
578 format!("Required: {}.", yes_no(parameter.required)),
579 format!(
580 "Value: {}.",
581 if parameter.takes_value { "Yes" } else { "No" }
582 ),
583 ];
584
585 if options.include_usage {
586 parts.push(format!(
587 "Usage: `{}`.",
588 escape_markdown_text(¶meter.display)
589 ));
590 }
591
592 if !parameter.value_names.is_empty() {
593 parts.push(format!(
594 "Value name: {}.",
595 parameter
596 .value_names
597 .iter()
598 .map(|value| format!("`{}`", escape_markdown_text(value)))
599 .collect::<Vec<_>>()
600 .join(", ")
601 ));
602 }
603
604 if parameter.multiple {
605 parts.push("Multiple: Yes.".to_owned());
606 }
607
608 if !parameter.default_values.is_empty() {
609 parts.push(format!(
610 "Default value: {}.",
611 parameter
612 .default_values
613 .iter()
614 .map(|value| format!("`{}`", escape_markdown_text(value)))
615 .collect::<Vec<_>>()
616 .join(", ")
617 ));
618 }
619
620 if let Some(env) = ¶meter.env {
621 parts.push(format!("Environment: `{}`.", escape_markdown_text(env)));
622 }
623
624 if !parameter.possible_values.is_empty() {
625 parts.push(format!(
626 "Possible values: {}.",
627 parameter
628 .possible_values
629 .iter()
630 .map(|value| format!("`{}`", escape_markdown_text(value)))
631 .collect::<Vec<_>>()
632 .join(", ")
633 ));
634 }
635
636 output.push_str(&parts.join(" "));
637 output.push_str("\n\n");
638}
639
640fn command_info(command: &Command, command_path: &[String], heading_level: usize) -> CommandInfo {
641 CommandInfo {
642 name: command.get_name().to_owned(),
643 path: command_path.to_vec(),
644 display: command_path.join(" "),
645 description: command_description(command),
646 heading_level,
647 }
648}
649
650fn parameter_doc(arg: &Arg, command_path: &[String]) -> ParameterInfo {
651 let display = display_arg(arg);
652 let display_names = display_arg_names(arg);
653 let anchor_parts = command_path
654 .iter()
655 .map(String::as_str)
656 .chain(std::iter::once(arg.get_id().as_str()))
657 .collect::<Vec<_>>()
658 .join("-");
659
660 ParameterInfo {
661 anchor: slugify(&anchor_parts),
662 name: arg.get_id().to_string(),
663 display,
664 display_names,
665 description: arg_description(arg),
666 required: arg.is_required_set(),
667 multiple: arg_allows_multiple(arg),
668 takes_value: arg_takes_value(arg),
669 value_names: arg
670 .get_value_names()
671 .unwrap_or_default()
672 .iter()
673 .map(ToString::to_string)
674 .collect(),
675 default_values: arg
676 .get_default_values()
677 .iter()
678 .map(|value| os_to_string(value.as_os_str()))
679 .collect(),
680 env: arg.get_env().map(os_to_string),
681 possible_values: possible_values(arg),
682 }
683}
684
685fn display_arg(arg: &Arg) -> String {
686 display_arg_with_values(arg, true)
687}
688
689fn display_arg_names(arg: &Arg) -> String {
690 display_arg_with_values(arg, false)
691}
692
693fn display_arg_with_values(arg: &Arg, include_values: bool) -> String {
694 let mut names = Vec::new();
695
696 if let Some(short) = arg.get_short() {
697 names.push(format!("-{}", short));
698 }
699
700 if let Some(long) = arg.get_long() {
701 names.push(format!("--{}", long));
702 }
703
704 if names.is_empty() {
705 names.push(format!("<{}>", arg.get_id()));
706 }
707
708 if !include_values {
709 return names.join(", ");
710 }
711
712 let value_names = arg
713 .get_value_names()
714 .unwrap_or_default()
715 .iter()
716 .map(|name| format!("<{}>", name))
717 .collect::<Vec<_>>();
718
719 if !value_names.is_empty() && arg_takes_value(arg) {
720 let values = value_names.join(" ");
721 names
722 .into_iter()
723 .map(|name| {
724 if name.starts_with('-') {
725 format!("{} {}", name, values)
726 } else {
727 name
728 }
729 })
730 .collect::<Vec<_>>()
731 .join(", ")
732 } else {
733 names.join(", ")
734 }
735}
736
737fn command_description(command: &Command) -> Option<String> {
738 command
739 .get_long_about()
740 .or_else(|| command.get_about())
741 .map(ToString::to_string)
742 .map(normalize_description)
743 .filter(|description| !description.is_empty())
744}
745
746fn arg_description(arg: &Arg) -> Option<String> {
747 arg.get_long_help()
748 .or_else(|| arg.get_help())
749 .map(ToString::to_string)
750 .map(normalize_description)
751 .filter(|description| !description.is_empty())
752}
753
754fn possible_values(arg: &Arg) -> Vec<String> {
755 arg.get_value_parser()
756 .possible_values()
757 .into_iter()
758 .flatten()
759 .map(|value| value.get_name().to_owned())
760 .collect()
761}
762
763fn arg_takes_value(arg: &Arg) -> bool {
764 matches!(arg.get_action(), ArgAction::Set | ArgAction::Append)
765}
766
767fn arg_allows_multiple(arg: &Arg) -> bool {
768 matches!(arg.get_action(), ArgAction::Append | ArgAction::Count)
769}
770
771fn is_generated_action(arg: &Arg) -> bool {
772 matches!(
773 arg.get_action(),
774 ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version
775 )
776}
777
778fn is_generated_help_subcommand(command: &Command) -> bool {
779 command.get_name() == "help"
780 && command_description(command)
781 .is_some_and(|description| description.starts_with("Print this message or the help"))
782}
783
784fn os_to_string(value: &OsStr) -> String {
785 value.to_string_lossy().into_owned()
786}
787
788fn push_heading(output: &mut String, level: usize, title: &str) {
789 output.push_str(&"#".repeat(level.max(1)));
790 output.push(' ');
791 output.push_str(title);
792 output.push_str("\n\n");
793}
794
795fn trim_trailing_blank_lines(output: &mut String) {
796 while output.ends_with('\n') {
797 output.pop();
798 }
799}
800
801fn push_custom_block(output: &mut String, block: &str) {
802 if block.is_empty() {
803 return;
804 }
805
806 output.push_str(block);
807
808 if !block.ends_with('\n') {
809 output.push('\n');
810 }
811
812 if !output.ends_with("\n\n") {
813 output.push('\n');
814 }
815}
816
817fn normalize_description(description: String) -> String {
818 description.trim().to_owned()
819}
820
821fn summary_line(description: &str) -> Option<&str> {
822 description
823 .lines()
824 .map(str::trim)
825 .find(|line| !line.is_empty())
826}
827
828fn yes_no(value: bool) -> &'static str {
829 if value { "Yes" } else { "No" }
830}
831
832fn escape_markdown_text(value: &str) -> String {
833 value.replace('[', "\\[").replace(']', "\\]")
834}
835
836fn escape_table_cell(value: &str) -> String {
837 escape_markdown_text(value).replace('|', "\\|")
838}
839
840fn slugify(value: &str) -> String {
841 let mut slug = String::new();
842 let mut previous_dash = false;
843
844 for character in value.chars().flat_map(char::to_lowercase) {
845 if character.is_ascii_alphanumeric() {
846 slug.push(character);
847 previous_dash = false;
848 } else if !previous_dash && !slug.is_empty() {
849 slug.push('-');
850 previous_dash = true;
851 }
852 }
853
854 if slug.ends_with('-') {
855 slug.pop();
856 }
857
858 slug
859}