Skip to main content

secunit_core/wisp/
template.rs

1//! The bundled generic partials, plus locating/validating an operator's
2//! template directory.
3//!
4//! Partials are required, operator-owned files. `init` materialises the
5//! bundled defaults below; `export` checks that the required set is present
6//! before rendering and otherwise fails with a pointer to `wisp init`.
7
8use std::path::{Path, PathBuf};
9
10use anyhow::{bail, Result};
11
12use super::Format;
13
14/// Default template directory, relative to the registry root.
15pub const DEFAULT_DIR: &str = "templates/wisp";
16
17/// The bundled generic Typst partials, embedded into the binary.
18pub const TYPST_PARTIALS: &[(&str, &str)] = &[
19    ("theme.typ", include_str!("assets/typst/theme.typ")),
20    ("header.typ", include_str!("assets/typst/header.typ")),
21    ("footer.typ", include_str!("assets/typst/footer.typ")),
22    ("cover.typ", include_str!("assets/typst/cover.typ")),
23    ("toc.typ", include_str!("assets/typst/toc.typ")),
24];
25
26/// The bundled default logo (the secunit shield).
27pub const LOGO_SVG: &str = include_str!("assets/logo.svg");
28
29/// The bundled partials for a format. HTML partials are not bundled yet (the
30/// HTML backends are opt-in / not wired), so requesting them is an error.
31pub fn bundled(format: Format) -> Result<&'static [(&'static str, &'static str)]> {
32    match format {
33        Format::Typst => Ok(TYPST_PARTIALS),
34        Format::Html => bail!(
35            "HTML partials are not available yet — the HTML render backends \
36             (WeasyPrint/Chromium) are opt-in and not wired. Use `--format typst`."
37        ),
38    }
39}
40
41/// Detect which partial format a template directory holds, by probing for the
42/// format-defining theme file.
43pub fn detect_format(dir: &Path) -> Option<Format> {
44    if dir.join("theme.typ").exists() {
45        Some(Format::Typst)
46    } else if dir.join("theme.css").exists() {
47        Some(Format::Html)
48    } else {
49        None
50    }
51}
52
53/// Confirm every required partial for `format` exists in `dir`. Returns the
54/// list of missing filenames (empty == complete).
55pub fn missing_partials(dir: &Path, format: Format) -> Vec<&'static str> {
56    format
57        .required_partials()
58        .iter()
59        .copied()
60        .filter(|name| !dir.join(name).exists())
61        .collect()
62}
63
64/// Fail with an actionable error if the template directory is absent or
65/// incomplete. This is the gate that makes the partials *required*.
66pub fn require_complete(dir: &Path, format: Format) -> Result<()> {
67    if !dir.exists() {
68        bail!(
69            "WISP template directory `{}` does not exist. Run `secunit wisp init` \
70             to scaffold the {} partials, review and commit them, then re-run export.",
71            dir.display(),
72            format
73        );
74    }
75    let missing = missing_partials(dir, format);
76    if !missing.is_empty() {
77        bail!(
78            "WISP template `{}` is missing required partial(s): {}. Run \
79             `secunit wisp init --force` to restore the defaults, or add them by hand.",
80            dir.display(),
81            missing.join(", ")
82        );
83    }
84    Ok(())
85}
86
87/// Resolve the template directory: explicit override, else `<root>/templates/wisp`.
88pub fn resolve_dir(root: &Path, override_dir: Option<&Path>) -> PathBuf {
89    match override_dir {
90        Some(d) if d.is_absolute() => d.to_path_buf(),
91        Some(d) => root.join(d),
92        None => root.join(DEFAULT_DIR),
93    }
94}