facet-wrapped/src/docs.rs
//! **The documents family** (2026-09-29, the last "no data seam yet" crates): the
//! vector-page document viewer and the searchable markdown manual.
//!
//! Both render text through facett-docview's resvg engine, whose airgap font used to be
//! an embedded 760 KB DejaVu Sans. This bundle depends on both with `default-features =
//! false`, so their `dejavu` feature is OFF here and the airgap font is egui's own
//! Ubuntu-Light — the same bytes egui links for its default fonts — so wrapping them
//! adds no font to the bundle (measured in `Cargo.toml`, beside the dependencies).
use facett_core::Facet;
use serde_json::Value;
use crate::registry::{Entry, Registry, UI};
/// One tiny page with real `<text>`, so the fixture exercises the text layer.
const FIXTURE_SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="595" height="842"><rect width="595" height="842" fill="#fff"/><text x="60" y="100" font-size="28">facett docview</text><text x="60" y="150" font-size="14">a vector page, parsed and rasterised by resvg</text></svg>"##;
/// Register the documents family.
pub(crate) fn register(r: &mut Registry) {
r.register(
Entry::new("docview", "A vector-page document viewer: SVG pages, table of contents, zoom, search.", || {
docview(&serde_json::json!({ "title": "docview", "pages": [FIXTURE_SVG] })).expect("the fixture page parses")
})
.in_group(UI)
.with_data(docview),
);
r.register(
Entry::new("manual", "A searchable markdown manual: sidebar of documents, rendered markdown.", || {
Box::new(facett_manual::ManualPane::from_files(
"facett",
[("guide.md", "# facett manual\n\nEvery component, one document each.\n")],
))
})
.in_group(UI)
.with_data(manual),
);
}
fn s<'a>(v: &'a Value, k: &str) -> &'a str {
v.get(k).and_then(Value::as_str).unwrap_or("")
}
/// **`docview`** — [`facett_docview::DocView`].
///
/// ```json
/// { "title": "handbook",
/// "pages": ["<svg …>…</svg>", "<svg …>…</svg>"],
/// "toc": [{ "title": "Intro", "page": 0 }] }
/// ```
/// Each page is an SVG document (a Typst export is one SVG per page). A page that does
/// not parse is a refusal of the whole payload — a document with a silently missing
/// page is not the document. No pages is a refusal. A toc entry past the last page is
/// dropped.
#[must_use]
pub fn docview(v: &Value) -> Option<Box<dyn Facet>> {
let pages: Vec<facett_docview::DocPage> = v
.get("pages")
.and_then(Value::as_array)?
.iter()
.map(|p| p.as_str().and_then(|svg| facett_docview::DocPage::from_svg_bytes(svg.as_bytes()).ok()))
.collect::<Option<Vec<_>>>()?;
if pages.is_empty() {
return None;
}
let toc = v
.get("toc")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|t| {
let page = t.get("page").and_then(Value::as_u64)? as usize;
(page < pages.len()).then(|| (s(t, "title").to_string(), page))
})
.collect()
})
.unwrap_or_default();
let title = if s(v, "title").is_empty() { "document" } else { s(v, "title") };
Some(Box::new(facett_docview::DocView::new(title, pages, toc)))
}
/// **`manual`** — [`facett_manual::ManualPane::from_files`].
///
/// ```json
/// { "app": "korp", "files": [{ "name": "infra-for-idiots.md", "body": "# Infra\n…" }] }
/// ```
/// The component derives each document's slug, kind and title from its file name and
/// first heading, and skips template/scaffold files, exactly as it does for an
/// embedded manual. No files (after that skip) is a refusal.
#[must_use]
pub fn manual(v: &Value) -> Option<Box<dyn Facet>> {
let files: Vec<(&str, String)> = v
.get("files")
.and_then(Value::as_array)?
.iter()
.filter_map(|f| {
let name = f.get("name").and_then(Value::as_str).filter(|n| !n.trim().is_empty())?;
Some((name, s(f, "body").to_string()))
})
.filter(|(name, _)| !facett_manual::ManualDoc::is_template(name))
.collect();
if files.is_empty() {
return None;
}
let app = if s(v, "app").is_empty() { "manual" } else { s(v, "app") };
Some(Box::new(facett_manual::ManualPane::from_files(app, files)))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn docview_parses_every_page_or_refuses_the_document() {
let ok = json!({ "title": "t", "pages": [FIXTURE_SVG, FIXTURE_SVG], "toc": [{ "title": "b", "page": 1 }, { "title": "x", "page": 9 }] });
let st = docview(&ok).expect("builds").state_json();
assert!(st.to_string().contains("\"b\""), "the in-range toc entry is kept: {st}");
assert!(!st.to_string().contains("\"x\""), "the out-of-range toc entry is dropped: {st}");
assert!(docview(&json!({ "pages": [FIXTURE_SVG, "<not svg"] })).is_none(), "a bad page refuses the document");
assert!(docview(&json!({ "pages": [] })).is_none());
}
#[test]
fn manual_takes_named_markdown_files_and_skips_templates() {
let v = json!({ "app": "korp", "files": [{ "name": "infra-for-idiots.md", "body": "# Infra\nhello" },
{ "name": "_TEMPLATE-guide.md", "body": "# T" }] });
let st = manual(&v).expect("builds").state_json();
assert_eq!(st["doc_count"], 1, "the template is skipped: {st}");
assert!(st.to_string().contains("infra-for-idiots"), "the slug comes from the file name: {st}");
assert!(manual(&json!({ "files": [{ "name": "_TEMPLATE-x.md", "body": "" }] })).is_none());
}
}