use std::fmt::Write;
use clap::{Arg, ArgAction, Command};
use crate::options::Options;
pub(crate) fn render(root: &Command, opts: &Options) -> String {
let mut buf = String::new();
let path = vec![canonical_name(root)];
if opts.table_of_contents {
buf.push_str("**Contents:**\n\n");
write_toc(&mut buf, root, &path, 0);
buf.push('\n');
}
render_command(&mut buf, root, &path, 0, opts);
if opts.footer {
buf.push_str("---\n\n");
buf.push_str("_Generated by [clapdown](https://crates.io/crates/clapdown)._\n");
}
let trimmed = buf.trim_end();
format!("{trimmed}\n")
}
fn render_command(buf: &mut String, cmd: &Command, path: &[String], depth: usize, opts: &Options) {
let level = opts.base_heading_level as usize + depth;
let heading = if depth == 0 {
match &opts.title {
Some(title) => title.clone(),
None => format!("`{}`", path.join(" ")),
}
} else {
format!("`{}`", path.join(" "))
};
write_heading(buf, level, &heading);
if let Some(about) = cmd.get_long_about().or_else(|| cmd.get_about()) {
let _ = writeln!(buf, "{about}\n");
}
let _ = writeln!(buf, "**Usage:** `{}`\n", usage_line(cmd, path));
if opts.aliases {
let aliases: Vec<_> = cmd.get_visible_aliases().collect();
if !aliases.is_empty() {
let label = if aliases.len() == 1 {
"Alias"
} else {
"Aliases"
};
let joined = aliases
.iter()
.map(|a| format!("`{a}`"))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(buf, "**{label}:** {joined}\n");
}
}
let positionals: Vec<&Arg> = cmd
.get_arguments()
.filter(|a| a.is_positional() && !a.is_hide_set())
.collect();
if !positionals.is_empty() {
write_heading(buf, level + 1, "Arguments");
for arg in positionals {
buf.push_str(&render_arg(arg));
buf.push('\n');
}
}
let options: Vec<&Arg> = cmd
.get_arguments()
.filter(|a| !a.is_positional() && !a.is_hide_set())
.filter(|a| a.get_id() != "help" && a.get_id() != "version")
.collect();
if !options.is_empty() {
write_heading(buf, level + 1, "Options");
for arg in options {
buf.push_str(&render_arg(arg));
buf.push('\n');
}
}
for sub in cmd.get_subcommands().filter(|s| !s.is_hide_set()) {
let mut child_path = path.to_vec();
child_path.push(sub.get_name().to_string());
render_command(buf, sub, &child_path, depth + 1, opts);
}
}
fn write_heading(buf: &mut String, level: usize, text: &str) {
if level <= 6 {
let _ = writeln!(buf, "{} {text}\n", "#".repeat(level));
} else {
let _ = writeln!(buf, "**{text}**\n");
}
}
fn render_arg(arg: &Arg) -> String {
let term = arg_term(arg);
let mut blocks: Vec<String> = Vec::new();
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
blocks.push(help.to_string());
}
let defaults: Vec<String> = arg
.get_default_values()
.iter()
.map(|v| format!("`{}`", v.to_string_lossy()))
.collect();
if !defaults.is_empty() {
let label = if defaults.len() == 1 {
"Default value"
} else {
"Default values"
};
blocks.push(format!("{label}: {}", defaults.join(", ")));
}
if !matches!(arg.get_action(), ArgAction::SetTrue | ArgAction::SetFalse) {
let possible: Vec<_> = arg
.get_possible_values()
.into_iter()
.filter(|pv| !pv.is_hide_set())
.collect();
if !possible.is_empty() {
if possible.iter().any(|pv| pv.get_help().is_some()) {
let mut list = String::from("Possible values:\n");
for pv in &possible {
match pv.get_help() {
Some(help) => {
let _ = write!(list, "\n- `{}`: {help}", pv.get_name());
}
None => {
let _ = write!(list, "\n- `{}`", pv.get_name());
}
}
}
blocks.push(list);
} else {
let names = possible
.iter()
.map(|pv| format!("`{}`", pv.get_name()))
.collect::<Vec<_>>()
.join(", ");
blocks.push(format!("Possible values: {names}"));
}
}
}
definition(&term, &blocks)
}
fn arg_term(arg: &Arg) -> String {
if arg.is_positional() {
return format!("`{}`", positional_name(arg));
}
let mut tokens: Vec<String> = Vec::new();
if let Some(short) = arg.get_short() {
tokens.push(format!("-{short}"));
}
if let Some(long) = arg.get_long() {
tokens.push(format!("--{long}"));
}
if takes_value(arg)
&& let Some(last) = tokens.last_mut()
{
let _ = write!(last, " <{}>", value_name(arg));
}
tokens
.into_iter()
.map(|t| format!("`{t}`"))
.collect::<Vec<_>>()
.join(", ")
}
fn definition(term: &str, blocks: &[String]) -> String {
let mut out = String::new();
let _ = writeln!(out, "{term}");
let body = blocks.join("\n\n");
let mut wrote_marker = false;
for line in body.lines() {
if !wrote_marker {
if line.is_empty() {
out.push_str(":\n");
} else {
let _ = writeln!(out, ": {line}");
}
wrote_marker = true;
} else if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, " {line}");
}
}
if !wrote_marker {
out.push_str(":\n");
}
out
}
fn positional_name(arg: &Arg) -> String {
let mut name = format!("<{}>", value_name(arg));
if is_variadic(arg) {
name.push_str("...");
}
name
}
fn value_name(arg: &Arg) -> String {
if let Some(names) = arg.get_value_names()
&& let Some(first) = names.first()
{
return first.to_string();
}
arg.get_id().as_str().to_uppercase()
}
fn takes_value(arg: &Arg) -> bool {
matches!(arg.get_action(), ArgAction::Set | ArgAction::Append)
}
fn is_variadic(arg: &Arg) -> bool {
arg.get_num_args()
.map(|r| r.max_values() > 1)
.unwrap_or(false)
}
fn usage_line(cmd: &Command, path: &[String]) -> String {
let usage = cmd.clone().render_usage().to_string();
let body = usage.strip_prefix("Usage: ").unwrap_or(&usage).trim();
if path.len() > 1 {
format!("{} {body}", path[..path.len() - 1].join(" "))
} else {
body.to_string()
}
}
fn canonical_name(cmd: &Command) -> String {
cmd.get_display_name()
.or_else(|| cmd.get_bin_name())
.unwrap_or_else(|| cmd.get_name())
.to_string()
}
fn write_toc(buf: &mut String, cmd: &Command, path: &[String], indent: usize) {
let text = path.join(" ");
let anchor = slug(&format!("`{text}`"));
let _ = writeln!(buf, "{}- [`{text}`](#{anchor})", " ".repeat(indent));
for sub in cmd.get_subcommands().filter(|s| !s.is_hide_set()) {
let mut child_path = path.to_vec();
child_path.push(sub.get_name().to_string());
write_toc(buf, sub, &child_path, indent + 1);
}
}
fn slug(text: &str) -> String {
let mut out = String::new();
for c in text.chars() {
if c.is_alphanumeric() {
out.extend(c.to_lowercase());
} else if c == ' ' || c == '-' || c == '_' {
out.push('-');
}
}
out
}