mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
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:
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user