Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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());
    }
}