clap_doc_generator 0.1.0

Automatically generate CLI documentation from clap definitions and update readme files
use std::fmt::Write;

use super::model_command::ArgInfo;
use super::model_command::CommandInfo;

pub fn render_markdown(command: &CommandInfo) -> String {
    let mut output = String::new();

    let usage = build_usage_string(command);
    let _ = writeln!(output, "**Usage:** `{usage}`\n");

    if !command.about.is_empty() {
        let _ = writeln!(output, "{}\n", command.about);
    }

    let (positional, options): (Vec<&ArgInfo>, Vec<&ArgInfo>) = command
        .args
        .iter()
        .partition(|a| !a.signature.starts_with('-'));

    render_args_table(&mut output, "Arguments", &positional);
    render_args_table(&mut output, "Options", &options);

    if !command.subcommands.is_empty() {
        render_command_table(&mut output, &command.subcommands);

        for sub in &command.subcommands {
            render_subcommand_section(&mut output, sub, &command.name);
        }
    }

    output
}

fn build_usage_string(command: &CommandInfo) -> String {
    let mut parts = vec![command.name.clone()];

    if command.args.iter().any(|a| a.signature.starts_with('-')) {
        parts.push("[OPTIONS]".to_string());
    }

    for arg in &command.args {
        if !arg.signature.starts_with('-') {
            if arg.required {
                parts.push(arg.signature.clone());
            } else {
                parts.push(format!(
                    "[{}]",
                    arg.signature.trim_matches(|c| c == '<' || c == '>')
                ));
            }
        }
    }

    if !command.subcommands.is_empty() {
        parts.push("<COMMAND>".to_string());
    }

    parts.join(" ")
}

fn render_command_table(output: &mut String, subcommands: &[CommandInfo]) {
    let _ = writeln!(output, "#### Commands\n");
    let _ = writeln!(output, "| Command | Description |");
    let _ = writeln!(output, "|---------|-------------|");

    for sub in subcommands {
        let _ = writeln!(output, "| `{}` | {} |", sub.name, escape_pipes(&sub.about));
    }

    output.push('\n');
}

fn render_subcommand_section(output: &mut String, sub: &CommandInfo, parent_name: &str) {
    let full_name = format!("{parent_name} {}", sub.name);
    let _ = writeln!(output, "### `{full_name}`\n");

    if !sub.about.is_empty() {
        let _ = writeln!(output, "{}\n", sub.about);
    }

    let (positional, options): (Vec<&ArgInfo>, Vec<&ArgInfo>) =
        sub.args.iter().partition(|a| !a.signature.starts_with('-'));

    render_args_table(output, "Arguments", &positional);
    render_args_table(output, "Options", &options);

    if !sub.subcommands.is_empty() {
        render_command_table(output, &sub.subcommands);
        for child in &sub.subcommands {
            render_subcommand_section(output, child, &full_name);
        }
    }
}

fn render_args_table(output: &mut String, heading: &str, args: &[&ArgInfo]) {
    if args.is_empty() {
        return;
    }

    let has_defaults = args.iter().any(|a| a.default.is_some());
    let has_possible_values = args.iter().any(|a| !a.possible_values.is_empty());

    let _ = writeln!(output, "#### {heading}\n");

    let mut header = format!("| {heading} | Description |");
    let mut separator = "|------|------|".to_string();

    if has_defaults {
        header.push_str(" Default |");
        separator.push_str("------|");
    }
    if has_possible_values {
        header.push_str(" Values |");
        separator.push_str("------|");
    }

    let _ = writeln!(output, "{header}");
    let _ = writeln!(output, "{separator}");

    for arg in args {
        let mut row = format!(
            "| `{}` | {} |",
            escape_pipes(&arg.signature),
            escape_pipes(&arg.help),
        );

        if has_defaults {
            let default_cell = arg
                .default
                .as_deref()
                .map(|d| format!("`{}`", escape_pipes(d)))
                .unwrap_or_default();
            row.push_str(&format!(" {default_cell} |"));
        }

        if has_possible_values {
            let values_cell = if arg.possible_values.is_empty() {
                String::new()
            } else {
                arg.possible_values
                    .iter()
                    .map(|v| format!("`{v}`"))
                    .collect::<Vec<_>>()
                    .join(", ")
            };
            row.push_str(&format!(" {values_cell} |"));
        }

        let _ = writeln!(output, "{row}");
    }

    output.push('\n');
}

fn escape_pipes(text: &str) -> String {
    text.replace('|', "\\|")
}