125 lines
5.6 KiB
Rust
125 lines
5.6 KiB
Rust
//! `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])),
|
||
}
|
||
}
|