Files
solidcraft-upstream/xtask/src/book.rs
T

125 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo xtask book`: generate the book's command reference and screenshots, then build it.
//!
//! The command reference (`docs/book/src/reference/commands.md`) comes from
//! `solidcraft-cli commands`, so it can't drift from the registry. The screenshots in
//! `docs/book/src/generated/` are GPU renders by `solidcraft-cli snapshot` of `examples/*.json`
//! and `docs/book/scripts/*.json`, plus the app screenshot from `docs/screenshots/`. Both are
//! gitignored and attributed in ATTRIBUTION.md. The book itself is built by `mdbook` (a separate
//! tool, not a dependency) into `target/book`.
use std::path::Path;
use serde_json::Value;
use crate::cargo;
/// Toolbar tabs in toolbar order; commands not on the toolbar come last, grouped by id prefix.
const TABS: &[&str] = &["SOLID", "SKETCH", "SURFACE", "SHEET METAL", "PLASTIC"];
const SNAPSHOT_SIZE: (&str, &str) = ("960", "600");
pub fn run(root: &Path, build: bool) -> Result<(), String> {
let book = root.join("docs/book");
std::fs::copy(root.join("assets/branding/web/source/og.png"), book.join("src/og.png")).map_err(|e| format!("book preview card: {e}"))?;
let mut c = cargo();
c.args(["build", "-q", "--release", "-p", "solidcraft-cli"]);
crate::run(c, "cargo build -p solidcraft-cli")?;
let out = cli().arg("commands").output().map_err(|e| format!("solidcraft-cli: {e}"))?;
if !out.status.success() {
return Err(format!("solidcraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr)));
}
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
let md = reference(cmds.as_array().map(Vec::as_slice).unwrap_or_default());
write(&book.join("src/reference/commands.md"), &md)?;
let gen_dir = book.join("src/generated");
std::fs::create_dir_all(&gen_dir).map_err(|e| format!("{}: {e}", gen_dir.display()))?;
let mut scripts = vec![root.join("examples/bracket.json")];
scripts.extend(json_files(&book.join("scripts"))?);
for s in &scripts {
let stem = s.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_default();
let png = gen_dir.join(format!("{stem}.png"));
let mut c = cli();
c.arg("snapshot").arg(s).arg("--out").arg(&png).args(["--width", SNAPSHOT_SIZE.0, "--height", SNAPSHOT_SIZE.1]);
let o = c.output().map_err(|e| format!("solidcraft-cli snapshot: {e}"))?;
if !o.status.success() {
return Err(format!("snapshot of {} failed:\n{}", s.display(), String::from_utf8_lossy(&o.stderr)));
}
println!("book: {}", png.strip_prefix(root).unwrap_or(&png).display());
}
let shot = "sample-plate.png";
std::fs::copy(root.join("docs/screenshots").join(shot), gen_dir.join(shot)).map_err(|e| format!("{shot}: {e}"))?;
if build {
let mut c = std::process::Command::new("mdbook");
c.arg("build").arg(&book);
crate::run(c, "mdbook build docs/book").map_err(|e| format!("{e}\n(install mdbook with `cargo install mdbook`)"))?;
println!("book: built into target/book");
}
Ok(())
}
/// The release solidcraft-cli just built.
fn cli() -> std::process::Command {
let mut c = cargo();
c.args(["run", "-q", "--release", "-p", "solidcraft-cli", "--"]);
c
}
fn write(path: &Path, text: &str) -> Result<(), String> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
std::fs::write(path, text).map_err(|e| format!("{}: {e}", path.display()))
}
fn json_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, String> {
let rd = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let mut v: Vec<_> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "json")).collect();
v.sort();
Ok(v)
}
/// The command reference page: one section per toolbar tab and panel, in registry order.
fn reference(cmds: &[Value]) -> String {
let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string();
let mut md = format!(
"# Command reference\n\nGenerated by `cargo xtask book` from `solidcraft-cli commands` ({} commands). Every command runs the same \
way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP. Parameters marked `?` are \
optional; `expr` is a number or an expression with units.\n",
cmds.len()
);
let mut sections: Vec<(String, Vec<&Value>)> = Vec::new();
for tab in TABS {
for c in cmds.iter().filter(|c| s(c, "tab") == *tab) {
let title = format!("{tab} › {}", s(c, "panel"));
add(&mut sections, title, c);
}
}
for c in cmds.iter().filter(|c| !TABS.contains(&s(c, "tab").as_str())) {
let id = s(c, "id");
let prefix = id.split('.').next().unwrap_or_default().to_string();
add(&mut sections, format!("Other: {prefix}.*"), c);
}
for (title, list) in sections {
md.push_str(&format!("\n## {title}\n\n"));
for c in list {
let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default();
md.push_str(&format!("- `{}` **{}**{key}", s(c, "id"), s(c, "label")));
let p = s(c, "params");
if !p.is_empty() {
md.push_str(&format!("<br>``{}``", p.replace('\n', " ")));
}
md.push('\n');
}
}
md
}
fn add<'a>(sections: &mut Vec<(String, Vec<&'a Value>)>, title: String, c: &'a Value) {
match sections.iter_mut().find(|(t, _)| *t == title) {
Some((_, v)) => v.push(c),
None => sections.push((title, vec![c])),
}
}