#[doc(hidden)]
mod test_readme {
#![doc = include_str!("../README.md")]
}
mod utils;
use std::fmt::{self, Write};
use clap::builder::PossibleValue;
use utils::pluralize;
#[non_exhaustive]
pub struct MarkdownOptions {
title: Option<String>,
show_footer: bool,
show_table_of_contents: bool,
show_aliases: bool,
}
impl MarkdownOptions {
pub fn new() -> Self {
return Self {
title: None,
show_footer: true,
show_table_of_contents: true,
show_aliases: true,
};
}
pub fn title(mut self, title: String) -> Self {
self.title = Some(title);
return self;
}
pub fn show_footer(mut self, show: bool) -> Self {
self.show_footer = show;
return self;
}
pub fn show_table_of_contents(mut self, show: bool) -> Self {
self.show_table_of_contents = show;
return self;
}
pub fn show_aliases(mut self, show: bool) -> Self {
self.show_aliases = show;
return self;
}
}
impl Default for MarkdownOptions {
fn default() -> Self {
return Self::new();
}
}
pub fn help_markdown<C: clap::CommandFactory>() -> String {
let command = C::command();
help_markdown_command(&command)
}
pub fn help_markdown_custom<C: clap::CommandFactory>(
options: &MarkdownOptions,
) -> String {
let command = C::command();
return help_markdown_command_custom(&command, options);
}
pub fn help_markdown_command(command: &clap::Command) -> String {
return help_markdown_command_custom(command, &Default::default());
}
pub fn help_markdown_command_custom(
command: &clap::Command,
options: &MarkdownOptions,
) -> String {
let mut buffer = String::with_capacity(100);
write_help_markdown(&mut buffer, &command, options);
buffer
}
pub fn print_help_markdown<C: clap::CommandFactory>() {
let command = C::command();
let mut buffer = String::with_capacity(100);
write_help_markdown(&mut buffer, &command, &Default::default());
println!("{}", buffer);
}
fn write_help_markdown(
buffer: &mut String,
command: &clap::Command,
options: &MarkdownOptions,
) {
let title_name = get_canonical_name(command);
let title = match options.title {
Some(ref title) => title.to_owned(),
None => format!("Command-Line Help for `{title_name}`"),
};
writeln!(buffer, "# {title}\n",).unwrap();
writeln!(
buffer,
"This document contains the help content for the `{}` command-line program.\n",
title_name
).unwrap();
if options.show_table_of_contents {
writeln!(buffer, "**Command Overview:**\n").unwrap();
build_table_of_contents_markdown(buffer, Vec::new(), command, 0)
.unwrap();
write!(buffer, "\n").unwrap();
}
build_command_markdown(buffer, Vec::new(), command, 0, options).unwrap();
if options.show_footer {
write!(buffer, r#"<hr/>
<small><i>
This document was generated automatically by
<a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
</i></small>
"#).unwrap();
}
}
fn build_table_of_contents_markdown(
buffer: &mut String,
parent_command_path: Vec<String>,
command: &clap::Command,
depth: usize,
) -> std::fmt::Result {
if command.is_hide_set() {
return Ok(());
}
let title_name = get_canonical_name(command);
let command_path = {
let mut command_path = parent_command_path;
command_path.push(title_name);
command_path
};
writeln!(
buffer,
"* [`{}`↴](#{})",
command_path.join(" "),
command_path.join("-"),
)?;
for subcommand in command.get_subcommands() {
build_table_of_contents_markdown(
buffer,
command_path.clone(),
subcommand,
depth + 1,
)?;
}
Ok(())
}
fn build_command_markdown(
buffer: &mut String,
parent_command_path: Vec<String>,
command: &clap::Command,
depth: usize,
options: &MarkdownOptions,
) -> std::fmt::Result {
if command.is_hide_set() {
return Ok(());
}
let title_name = get_canonical_name(command);
let command_path = {
let mut command_path = parent_command_path.clone();
command_path.push(title_name);
command_path
};
writeln!(buffer, "## `{}`\n", command_path.join(" "))?;
if let Some(long_about) = command.get_long_about() {
writeln!(buffer, "{}\n", long_about)?;
} else if let Some(about) = command.get_about() {
writeln!(buffer, "{}\n", about)?;
}
if let Some(help) = command.get_before_long_help() {
writeln!(buffer, "{}\n", help)?;
} else if let Some(help) = command.get_before_help() {
writeln!(buffer, "{}\n", help)?;
}
writeln!(
buffer,
"**Usage:** `{}{}`\n",
if parent_command_path.is_empty() {
String::new()
} else {
let mut s = parent_command_path.join(" ");
s.push_str(" ");
s
},
command
.clone()
.render_usage()
.to_string()
.replace("Usage: ", "")
)?;
if options.show_aliases {
let aliases = command.get_visible_aliases().collect::<Vec<&str>>();
if let Some(aliases_str) = get_alias_string(&aliases) {
writeln!(
buffer,
"**{}:** {aliases_str}\n",
pluralize(aliases.len(), "Command Alias", "Command Aliases")
)?;
}
}
if let Some(help) = command.get_after_long_help() {
writeln!(buffer, "{}\n", help)?;
} else if let Some(help) = command.get_after_help() {
writeln!(buffer, "{}\n", help)?;
}
if command.get_subcommands().next().is_some() {
writeln!(buffer, "###### **Subcommands:**\n")?;
for subcommand in command.get_subcommands() {
if subcommand.is_hide_set() {
continue;
}
let title_name = get_canonical_name(subcommand);
let about = match subcommand.get_about() {
Some(about) => about.to_string(),
None => String::new(),
};
writeln!(buffer, "* `{title_name}` — {about}",)?;
}
write!(buffer, "\n")?;
}
if command.get_positionals().next().is_some() {
writeln!(buffer, "###### **Arguments:**\n")?;
for pos_arg in command.get_positionals() {
write_arg_markdown(buffer, pos_arg)?;
}
write!(buffer, "\n")?;
}
let non_pos: Vec<_> = command
.get_arguments()
.filter(|arg| !arg.is_positional() && !arg.is_hide_set())
.collect();
if !non_pos.is_empty() {
writeln!(buffer, "###### **Options:**\n")?;
for arg in non_pos {
write_arg_markdown(buffer, arg)?;
}
write!(buffer, "\n")?;
}
write!(buffer, "\n\n")?;
for subcommand in command.get_subcommands() {
build_command_markdown(
buffer,
command_path.clone(),
subcommand,
depth + 1,
options,
)?;
}
Ok(())
}
fn write_arg_markdown(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
write!(buffer, "* ")?;
let value_name: String = match arg.get_value_names() {
Some([name, ..]) => name.as_str().to_owned(),
Some([]) => unreachable!(
"clap Arg::get_value_names() returned Some(..) of empty list"
),
None => arg.get_id().to_string().to_ascii_uppercase(),
};
match (arg.get_short(), arg.get_long()) {
(Some(short), Some(long)) => {
if arg.get_action().takes_values() {
write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
} else {
write!(buffer, "`-{short}`, `--{long}`")?
}
},
(Some(short), None) => {
if arg.get_action().takes_values() {
write!(buffer, "`-{short} <{value_name}>`")?
} else {
write!(buffer, "`-{short}`")?
}
},
(None, Some(long)) => {
if arg.get_action().takes_values() {
write!(buffer, "`--{} <{value_name}>`", long)?
} else {
write!(buffer, "`--{}`", long)?
}
},
(None, None) => {
debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
write!(buffer, "`<{value_name}>`",)?;
},
}
if let Some(aliases) = arg.get_visible_aliases().as_deref() {
if let Some(aliases_str) = get_alias_string(aliases) {
write!(
buffer,
" [{}: {aliases_str}]",
pluralize(aliases.len(), "alias", "aliases")
)?;
}
}
if let Some(help) = arg.get_long_help() {
buffer.push_str(&indent(&help.to_string(), " — ", " "))
} else if let Some(short_help) = arg.get_help() {
writeln!(buffer, " — {short_help}")?;
} else {
writeln!(buffer)?;
}
if !arg.get_default_values().is_empty() {
let default_values: String = arg
.get_default_values()
.iter()
.map(|value| format!("`{}`", value.to_string_lossy()))
.collect::<Vec<String>>()
.join(", ");
if arg.get_default_values().len() > 1 {
writeln!(buffer, "\n Default values: {default_values}")?;
} else {
writeln!(buffer, "\n Default value: {default_values}")?;
}
}
let possible_values: Vec<PossibleValue> = arg
.get_possible_values()
.into_iter()
.filter(|pv| !pv.is_hide_set())
.collect();
if !possible_values.is_empty()
&& !matches!(arg.get_action(), clap::ArgAction::SetTrue)
{
let any_have_help: bool =
possible_values.iter().any(|pv| pv.get_help().is_some());
if any_have_help {
let text: String = possible_values
.iter()
.map(|pv| match pv.get_help() {
Some(help) => {
format!(" - `{}`:\n {}\n", pv.get_name(), help)
},
None => format!(" - `{}`\n", pv.get_name()),
})
.collect::<Vec<String>>()
.join("");
writeln!(buffer, "\n Possible values:\n{text}")?;
} else {
let text: String = possible_values
.iter()
.map(|pv| format!("`{}`", pv.get_name()))
.collect::<Vec<String>>()
.join(", ");
writeln!(buffer, "\n Possible values: {text}\n")?;
}
}
Ok(())
}
fn get_canonical_name(command: &clap::Command) -> String {
command
.get_display_name()
.or_else(|| command.get_bin_name())
.map(|name| name.to_owned())
.unwrap_or_else(|| command.get_name().to_owned())
}
fn indent(s: &str, first: &str, rest: &str) -> String {
if s.is_empty() {
return "\n".to_string();
}
let mut result = String::new();
let mut first_line = true;
for line in s.lines() {
if !line.is_empty() {
result.push_str(if first_line { first } else { rest });
result.push_str(line);
first_line = false;
}
result.push('\n');
}
result
}
fn get_alias_string(aliases: &[&str]) -> Option<String> {
if aliases.is_empty() {
return None;
}
Some(format!(
"{}",
aliases
.iter()
.map(|alias| format!("`{alias}`"))
.collect::<Vec<_>>()
.join(", ")
))
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
#[test]
fn test_indent() {
use super::indent;
assert_eq!(
&indent("Header\n\nMore info", "___", "~~~~"),
"___Header\n\n~~~~More info\n"
);
assert_eq!(
&indent("Header\n\nMore info\n", "___", "~~~~"),
&indent("Header\n\nMore info", "___", "~~~~"),
);
assert_eq!(&indent("", "___", "~~~~"), "\n");
assert_eq!(&indent("\n", "___", "~~~~"), "\n");
}
}