feat: Improve admin command reference generation

- Change xtasks to use `clap` for argument parsing
- Generate admin command reference manually instead of with `clap_markdown`
- Split admin command reference into multiple files
This commit is contained in:
Ginger
2026-01-09 17:05:34 -05:00
parent 60dd6baffd
commit 89be9d1efc
31 changed files with 1297 additions and 5822 deletions
@@ -0,0 +1,112 @@
//! Generates documentation for the various commands that may be used in the admin room and server console.
//!
//! This generates one index page and several category pages, one for each of the direct subcommands of the top-level
//! `!admin` command. Those category pages then list all of the sub-subcommands.
use std::path::Path;
use askama::Template;
use clap::{Command, CommandFactory};
use conduwuit_admin::AdminCommand;
use crate::tasks::{TaskResult, generate_docs::FileOutput};
#[derive(askama::Template)]
#[template(path = "admin/index.md")]
/// The template for the index page, which links to all of the category pages.
struct Index {
categories: Vec<Category>
}
/// A direct subcommand of the top-level `!admin` command.
#[derive(askama::Template)]
#[template(path = "admin/category.md")]
struct Category {
name: String,
description: String,
commands: Vec<Subcommand>,
}
/// A second-or-deeper level subcommand of the `!admin` command.
struct Subcommand {
name: String,
description: String,
/// How deeply nested this command was in the original command tree.
/// This determines the header size used for it in the documentation.
depth: usize,
}
fn flatten_subcommands(command: &Command) -> Vec<Subcommand> {
let mut subcommands = Vec::new();
let mut name_stack = Vec::new();
fn flatten(
subcommands: &mut Vec<Subcommand>,
stack: &mut Vec<String>,
command: &Command
) {
let depth = stack.len();
stack.push(command.get_name().to_owned());
// do not include the root command
if depth > 0 {
let name = stack.join(" ");
let description = command
.get_long_about()
.or_else(|| command.get_about())
.map(|about| about.to_string())
.unwrap_or("_(no description)_".to_owned());
subcommands.push(
Subcommand {
name,
description,
depth,
}
);
}
for command in command.get_subcommands() {
flatten(subcommands, stack, command);
}
stack.pop();
}
flatten(&mut subcommands, &mut name_stack, command);
subcommands
}
pub(super) fn generate(out: &mut impl FileOutput) -> TaskResult<()> {
let admin_commands = AdminCommand::command();
let categories: Vec<_> = admin_commands
.get_subcommands()
.map(|command| {
Category {
name: command.get_name().to_owned(),
description: command.get_about().expect("categories should have a docstring").to_string(),
commands: flatten_subcommands(command),
}
})
.collect();
let root = Path::new("reference/admin/");
for category in &categories {
out.create_file(
root.join(&category.name).with_extension("md"),
category.render()?
);
}
out.create_file(
root.join("index.md"),
Index { categories }.render()?,
);
Ok(())
}
+67
View File
@@ -0,0 +1,67 @@
mod admin_commands;
use std::{collections::HashMap, path::{Path, PathBuf}};
use cargo_metadata::MetadataCommand;
use crate::tasks::TaskResult;
trait FileOutput {
fn create_file(&mut self, path: PathBuf, contents: String);
}
#[derive(Default)]
struct FileQueue {
queue: HashMap<PathBuf, String>,
}
impl FileQueue {
fn write(self, root: &Path, dry_run: bool) -> std::io::Result<()> {
for (path, contents) in self.queue.into_iter() {
let path = root.join(&path);
eprintln!("Writing {}", path.display());
if !dry_run {
std::fs::write(path, contents)?;
}
}
Ok(())
}
}
impl FileOutput for FileQueue {
fn create_file(&mut self, path: PathBuf, contents: String) {
assert!(path.is_relative(), "path must be relative");
assert!(path.extension().is_some(), "path must not point to a directory");
if self.queue.contains_key(&path) {
panic!("attempted to create an already created file {}", path.display());
}
self.queue.insert(path, contents);
}
}
#[derive(clap::Args)]
pub(crate) struct Args {
/// The base path of the documentation. Defaults to `docs/` in the crate root.
root: Option<PathBuf>,
}
pub(super) fn run(common_args: crate::Args, task_args: Args) -> TaskResult<()> {
let mut queue = FileQueue::default();
let metadata = MetadataCommand::new()
.no_deps()
.exec()
.expect("should have been able to run cargo");
let root = task_args.root.unwrap_or_else(|| metadata.workspace_root.join_os("docs/"));
admin_commands::generate(&mut queue)?;
queue.write(&root, common_args.dry_run)?;
Ok(())
}