cli_doc 0.1.2

Documentation generator for CLI
Documentation
use crate::commands::{CommandDesc, CommandId, ProgramDesc};
use askama::Template;
use askama::filters::{Escaper, Html};
use serde::Serialize;
use std::collections::HashMap;

#[derive(Template)]
#[template(path = "page.html")]
struct PageTemplate<'a> {
    project_name: &'a str,
    version: &'a str,
    command: CommandTemplate<'a>,
    command_json: String,
    generator: String,
}

#[derive(Template)]
#[template(path = "command.html", escape = "none")]
struct CommandTemplate<'a> {
    name: &'a str,
    id: CommandId,
    depth: u32,
    subcommands: Vec<CommandTemplate<'a>>,
}

#[derive(Serialize)]
struct OptionJson<'a> {
    id: String,
    short: Option<&'a str>,
    long: String,
    brief: String,
    description: Option<String>,
}

#[derive(Serialize)]
struct ArgumentJson {
    name: String,
    brief: String,
    description: Option<String>,
}

#[derive(Serialize)]
struct CategoryJson<'a> {
    title: &'a str,
    options: Vec<OptionJson<'a>>,
}

#[derive(Serialize)]
struct CommandJson<'a> {
    name: String,
    parent: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    children: Vec<String>,
    brief: String,
    description: Option<String>,
    usages: Vec<String>,
    arguments: Vec<ArgumentJson>,
    categories: Vec<CategoryJson<'a>>,
}

impl<'a> CommandJson<'a> {
    pub fn new(desc: &'a CommandDesc, parent: Option<&str>) -> Self {
        let mut option_id = 0;
        CommandJson {
            name: desc.name.to_string(),
            parent: parent.map(|x| x.to_string()),
            children: desc.commands.iter().map(|c| format!("c{}", c.id)).collect(),
            brief: desc.doc.brief.to_html(),
            description: desc.doc.description.as_ref().map(|t| t.to_html()),
            usages: desc.doc.usage.iter().map(|u| u.to_html()).collect(),
            arguments: if desc.doc.is_args_effectively_empty() {
                Vec::new()
            } else {
                desc.doc
                    .arguments
                    .iter()
                    .map(|a| ArgumentJson {
                        name: escape_html(&a.name),
                        brief: a.brief.to_html(),
                        description: a.description.as_ref().map(|t| t.to_html()),
                    })
                    .collect()
            },
            categories: desc
                .doc
                .option_categories
                .iter()
                .map(|c| CategoryJson {
                    title: &c.title,
                    options: c
                        .options
                        .iter()
                        .map(|o| OptionJson {
                            id: {
                                option_id += 1;
                                format!("o{}-{}", desc.id, option_id)
                            },
                            short: o.short.as_deref(),
                            long: escape_html(&o.long),
                            brief: o.brief.to_html(),
                            description: o.description.as_ref().map(|t| t.to_html()),
                        })
                        .collect(),
                })
                .collect(),
        }
    }
}

fn build_command_json<'a>(
    command: &'a CommandDesc,
    parent: Option<&str>,
    out: &mut HashMap<String, CommandJson<'a>>,
) {
    let id = format!("c{}", command.id);
    for c in &command.commands {
        build_command_json(c, Some(id.as_str()), out);
    }
    out.insert(id, CommandJson::new(command, parent));
}

fn escape_html(s: &str) -> String {
    let html = Html;
    let mut out = String::new();
    html.write_escaped_str(&mut out, s).unwrap();
    out
}

fn build_command_tree<'a>(command: &'a CommandDesc, depth: u32) -> CommandTemplate<'a> {
    let subcommands = command
        .commands
        .iter()
        .map(|c| build_command_tree(c, depth + 1))
        .collect();
    CommandTemplate {
        name: &command.name,
        id: command.id,
        subcommands,
        depth,
    }
}

pub fn render_html(program: &ProgramDesc) -> anyhow::Result<String> {
    let command_template = build_command_tree(&program.command, 0);
    let mut command_jsons: HashMap<String, CommandJson> = Default::default();
    build_command_json(&program.command, None, &mut command_jsons);

    Ok(PageTemplate {
        project_name: &program.command.name,
        version: &program.version,
        command: command_template,
        command_json: serde_json::to_string(&command_jsons)?,
        generator: format!("cli-doc {}", env!("CARGO_PKG_VERSION")),
    }
    .render()?)
}