dwarves-gui/src/manual.rs
//! 📖 **Manual** tab — dwarves' own documentation, embedded in the binary and rendered
//! live in the tabbed shell.
//!
//! Two variants, one pane, both dwarves' REAL docs:
//!
//! - **Typeset (SVG)** — the **default**: a crisp, book-like typeset render of
//! dwarves' start-here `.nornir` docs (README + the `*-for-idiots.md` beginner
//! guides). The SVG pages are prebuilt OFFLINE by `cargo xtask-docs docs` (which
//! runs dwarves' real markdown through nornir's `docs-export` markdown→typst→SVG
//! pipeline) and committed under `assets/manual-svg/`; the shipped UI binary just
//! `include`s them via [`rust_embed`] and renders them with facett-docview's
//! pure-Rust resvg engine — **no Typst at runtime**, no warehouse touched.
//! - **Markdown** — behind the header toggle: the FULL `.nornir/*.md` doc set (the
//! idiot guides, the how-to guides, the design notes), rendered live through the
//! shared markdown renderer, searchable.
//!
//! With the `manual-embed` feature (default) both are baked into the binary at compile
//! time, so the manual travels with the installed binary and renders offline. The pane
//! itself is the SHARED [`facett_manual::ManualPane`] — the SAME manual-pane core korp
//! mounts (L5: one component, not two).
//!
//! Built `--no-default-features` (manual-embed OFF), [`manual_pane`] returns the honest
//! [`ManualPane::not_embedded`] state — the tab says "manual not embedded in this build",
//! never a blank pane.
use facett_manual::ManualPane;
/// dwarves' embedded `.nornir/*.md` doc set (compile-time via rust_embed). Template
/// scaffolds (`_TEMPLATE-*.md`) are excluded here AND again by `ManualPane::from_files`.
/// This is the FULL set — the Markdown variant (toggle).
#[cfg(feature = "manual-embed")]
#[derive(rust_embed::RustEmbed)]
#[folder = "../.nornir/"]
#[include = "*.md"]
#[exclude = "_*.md"]
struct DwarvesDocs;
/// dwarves' prebuilt **typeset SVG book** — the committed output of
/// `cargo xtask-docs docs` (real `.nornir` docs → nornir docs-export → per-page SVG).
/// `page-NNN.svg` are the reading-ordered vector pages (the Typeset variant, default);
/// `toc.tsv` maps each chapter to its 0-based start page. Compile-time via rust_embed.
#[cfg(feature = "manual-embed")]
#[derive(rust_embed::RustEmbed)]
#[folder = "assets/manual-svg/"]
#[include = "*.svg"]
#[include = "toc.tsv"]
struct DwarvesManualSvg;
/// Decode the embedded typeset book: the SVG pages in reading order (sorted by the
/// zero-padded `page-NNN.svg` name) and the chapter TOC parsed from `toc.tsv`
/// (`<0-based page index>\t<title>` per line). Returns `(pages, toc)`; an absent /
/// empty book yields empties, and the pane then degrades to Markdown-only.
#[cfg(feature = "manual-embed")]
fn embedded_typeset_book() -> (Vec<Vec<u8>>, Vec<(String, usize)>) {
let mut names: Vec<String> = DwarvesManualSvg::iter()
.filter(|n| n.ends_with(".svg"))
.map(|n| n.to_string())
.collect();
names.sort();
let pages: Vec<Vec<u8>> = names
.iter()
.filter_map(|n| DwarvesManualSvg::get(n).map(|f| f.data.into_owned()))
.collect();
let toc: Vec<(String, usize)> = DwarvesManualSvg::get("toc.tsv")
.and_then(|f| String::from_utf8(f.data.into_owned()).ok())
.map(|s| {
s.lines()
.filter_map(|line| {
let (idx, title) = line.split_once('\t')?;
Some((title.trim().to_string(), idx.trim().parse::<usize>().ok()?))
})
.collect()
})
.unwrap_or_default();
(pages, toc)
}
/// Build the 📖 Manual tab pane. With `manual-embed` (default) it mounts the merged
/// dual-variant pane — **Typeset (SVG) by default** over dwarves' real start-here docs,
/// with the FULL `.nornir/*.md` set behind the Markdown toggle; without it, the honest
/// empty state.
pub fn manual_pane() -> ManualPane {
#[cfg(feature = "manual-embed")]
{
// Decode each embedded markdown file to (name, utf8 body); non-utf8 (none
// expected) skipped. `iter`/`get` are inherent methods from the RustEmbed derive.
let files: Vec<(String, String)> = DwarvesDocs::iter()
.filter_map(|name| {
let f = DwarvesDocs::get(&name)?;
let body = String::from_utf8(f.data.into_owned()).ok()?;
Some((name.to_string(), body))
})
.collect();
let refs: Vec<(&str, &str)> = files
.iter()
.map(|(n, b)| (n.as_str(), b.as_str()))
.collect();
// The prebuilt typeset book (default variant). If it is absent the pane
// degrades to Markdown-only — exactly the old `from_files` behavior.
let (svg_pages, toc) = embedded_typeset_book();
ManualPane::from_files_with_svgs("dwarves", refs, svg_pages, toc)
}
#[cfg(not(feature = "manual-embed"))]
{
ManualPane::not_embedded("dwarves")
}
}
#[cfg(all(test, feature = "manual-embed"))]
mod tests {
use super::*;
/// The embedded manual carries dwarves' idiot guides — `dwarves-for-idiots` must be
/// reachable in the pane (the guide a newcomer starts from).
#[test]
fn embeds_the_idiot_guides() {
let pane = manual_pane();
assert!(pane.is_embedded(), "manual-embed build must carry docs");
let slugs = pane.state_json()["docs"].as_array().unwrap().clone();
let has = |s: &str| slugs.iter().any(|v| v == s);
assert!(
has("dwarves-for-idiots"),
"dwarves-for-idiots.md must be reachable"
);
// Templates were excluded.
assert!(!has("_TEMPLATE-guide"), "template scaffolds are excluded");
}
/// The Manual defaults to the **Typeset (SVG)** variant rendered from dwarves' REAL
/// docs — NOT the generic facett sample, and not a markdown-only regression:
/// * the default variant is `typeset` and a typeset book is present with pages;
/// * the pane still carries dwarves' actual `.nornir` markdown (the toggle set),
/// proven by a known real slug from dwarves' own docs (`dwarves-for-idiots`);
/// * the typeset book has the pages the committed `assets/manual-svg/` render
/// produced (a real book, not a stub).
#[test]
fn default_variant_is_typeset_of_real_dwarves_docs() {
let pane = manual_pane();
let st = pane.state_json();
// Default = typeset (SVG), backed by a real book with pages.
assert_eq!(
st["variant"],
serde_json::json!("typeset"),
"default variant must be typeset (SVG)"
);
assert_eq!(
st["has_typeset"],
serde_json::json!(true),
"a typeset book is embedded"
);
assert!(pane.has_typeset(), "typeset book present");
let pages = st["typeset_page_count"].as_u64().unwrap_or(0);
assert!(
pages >= 5,
"the embedded typeset book has real pages, got {pages}"
);
// The content is dwarves' ACTUAL docs (real slug), not the facett sample.
let slugs = st["docs"].as_array().unwrap().clone();
assert!(
slugs.iter().any(|v| v == "dwarves-for-idiots"),
"typeset default must sit over dwarves' REAL docs (dwarves-for-idiots present), \
not the generic sample",
);
// The facett SAMPLE book's slugs must NOT be what we shipped.
assert!(
!slugs.iter().any(|v| v == "manual-for-idiots"),
"must not regress to facett-manual's embedded SAMPLE content",
);
// The embedded SVG book is real (parseable pages + a chapter TOC).
let (svg_pages, toc) = embedded_typeset_book();
assert!(
svg_pages.len() >= 5,
"committed SVG pages embedded, got {}",
svg_pages.len()
);
assert!(
svg_pages.iter().all(|p| p.windows(4).any(|w| w == b"<svg")),
"every embedded page is real SVG",
);
assert!(!toc.is_empty(), "the typeset book has a chapter TOC");
}
}