cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Embedded default theme — HTML templates, shortcode partials, CSS.
//!
//! Files under `src/infra/driven/site/default_theme/` are embedded into
//! the binary at compile time via [`include_dir!`]. Two enumerators expose
//! them to the rest of the site builder:
//!
//! - [`enumerate_templates`] — every `*.html` under `layouts/` and
//!   `shortcodes/`, keyed by its path relative to the embedded root.
//!   The Tera adapter feeds this into the registry; user theme overrides
//!   take precedence on name collision.
//! - [`enumerate_assets`] — every file under `assets/`, suitable for the
//!   site builder to copy verbatim into `<out>/assets/`.

use std::path::PathBuf;

use include_dir::{include_dir, Dir};

/// The compiled-in default theme tree. The macro path is resolved from
/// `CARGO_MANIFEST_DIR`, so it stays stable regardless of where `cargo`
/// is invoked from.
pub static DEFAULT_THEME: Dir<'_> =
    include_dir!("$CARGO_MANIFEST_DIR/src/infra/driven/site/default_theme");

/// Return every embedded HTML template as `(name, body)` where `name` is
/// the path relative to the embedded root (e.g. `layouts/index.html`,
/// `shortcodes/adr-ref.html`). Order is undefined — the Tera adapter
/// registers them all before user overrides.
pub fn enumerate_templates() -> Vec<(String, String)> {
    let mut out = Vec::new();
    collect_html(&DEFAULT_THEME, &mut out);
    out
}

fn collect_html(dir: &Dir<'_>, out: &mut Vec<(String, String)>) {
    for file in dir.files() {
        let path = file.path();
        let is_html = path.extension().is_some_and(|e| e == "html");
        let is_in_assets = path.starts_with("assets");
        if is_html && !is_in_assets {
            if let (Some(name), Some(body)) = (path.to_str(), file.contents_utf8()) {
                out.push((name.to_string(), body.to_string()));
            }
        }
    }
    for sub in dir.dirs() {
        collect_html(sub, out);
    }
}

/// Return every embedded static asset under `assets/` as `(relative_path,
/// bytes)`. The site builder copies them verbatim to `<out>/assets/`.
pub fn enumerate_assets() -> Vec<(PathBuf, &'static [u8])> {
    let mut out = Vec::new();
    if let Some(assets) = DEFAULT_THEME.get_dir("assets") {
        collect_assets(assets, &mut out);
    }
    out
}

fn collect_assets(dir: &Dir<'static>, out: &mut Vec<(PathBuf, &'static [u8])>) {
    for file in dir.files() {
        out.push((file.path().to_path_buf(), file.contents()));
    }
    for sub in dir.dirs() {
        collect_assets(sub, out);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn enumerates_at_least_the_homepage_layout() {
        let templates = enumerate_templates();
        let names: Vec<&str> = templates.iter().map(|(n, _)| n.as_str()).collect();
        assert!(names.contains(&"layouts/index.html"), "got: {names:?}");
        assert!(
            names.contains(&"layouts/_default/single.html"),
            "got: {names:?}"
        );
        assert!(
            names.contains(&"layouts/_default/list.html"),
            "got: {names:?}"
        );
        assert!(names.contains(&"layouts/base.html"), "got: {names:?}");
        assert!(names.contains(&"layouts/404.html"), "got: {names:?}");
    }

    #[test]
    fn enumerates_the_canonical_shortcodes() {
        let templates = enumerate_templates();
        let names: Vec<&str> = templates.iter().map(|(n, _)| n.as_str()).collect();
        for sc in [
            "shortcodes/adr-ref.html",
            "shortcodes/ddr-ref.html",
            "shortcodes/issue-ref.html",
            "shortcodes/callout.html",
            "shortcodes/toc.html",
        ] {
            assert!(names.contains(&sc), "missing {sc} in {names:?}");
        }
    }

    #[test]
    fn enumerates_assets_does_not_include_layouts() {
        let assets = enumerate_assets();
        let names: Vec<String> = assets
            .iter()
            .map(|(p, _)| p.to_string_lossy().to_string())
            .collect();
        assert!(
            names.iter().any(|n| n == "assets/style.css"),
            "got: {names:?}"
        );
        assert!(
            !names.iter().any(|n| n.contains("layouts")),
            "assets enumerator leaked layouts: {names:?}"
        );
    }

    #[test]
    fn template_bodies_are_non_empty() {
        for (name, body) in enumerate_templates() {
            assert!(!body.trim().is_empty(), "{name} body is empty");
        }
    }
}