use super::{doc::WispDoc, markdown, Status};
#[derive(Debug, Clone, Copy)]
pub struct EmitOptions {
pub toc: bool,
pub page_numbers: bool,
}
pub fn emit(doc: &WispDoc, opts: EmitOptions) -> String {
let m = &doc.meta;
let mut s = String::new();
s.push_str("// Generated by `secunit wisp export`. Do not edit by hand —\n");
s.push_str("// edit the partials in this directory instead, then re-export.\n\n");
s.push_str("#import \"theme.typ\": apply-theme, wisp-theme\n");
s.push_str("#import \"header.typ\": wisp-header\n");
s.push_str("#import \"footer.typ\": wisp-footer\n");
s.push_str("#import \"cover.typ\": wisp-cover\n");
s.push_str("#import \"toc.typ\": wisp-toc\n\n");
s.push_str("#let ctx = (\n");
push_field(&mut s, "org", &m.org);
push_field(&mut s, "title", &m.title);
push_field(&mut s, "version", &m.version);
push_field(&mut s, "effective_date", &m.effective_date);
push_field(&mut s, "classification", &m.classification);
push_field(&mut s, "status", &m.status.to_string());
push_field(&mut s, "logo", &m.logo);
push_field(&mut s, "commit", &m.commit);
push_field(&mut s, "content_hash", &m.content_hash);
push_field(&mut s, "generated_at", &m.generated_at);
s.push_str(")\n\n");
s.push_str("#set document(title: ctx.title, author: ctx.org)\n");
s.push_str("#set page(\n");
s.push_str(" paper: wisp-theme.paper,\n");
s.push_str(" margin: wisp-theme.margin,\n");
s.push_str(" header: wisp-header(ctx),\n");
if opts.page_numbers {
s.push_str(" footer: wisp-footer(ctx),\n");
}
if m.status == Status::Draft {
s.push_str(" background: rotate(-30deg, text(120pt, fill: rgb(\"#0000000d\"))[DRAFT]),\n");
}
s.push_str(" numbering: none,\n");
s.push_str(")\n");
s.push_str("#show: apply-theme\n\n");
s.push_str("#wisp-cover(ctx)\n\n");
if opts.toc {
s.push_str("#wisp-toc(ctx)\n\n");
}
s.push_str(&markdown::to_typst(&doc.body_markdown, &doc.sections));
s.push('\n');
s
}
fn push_field(s: &mut String, name: &str, value: &str) {
s.push_str(" ");
s.push_str(name);
s.push_str(": \"");
s.push_str(&markdown::escape_typst_string(value));
s.push_str("\",\n");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wisp::doc::WispMeta;
fn doc() -> WispDoc {
WispDoc {
meta: WispMeta {
org: "Battle \"Creek\"".into(),
title: "WISP".into(),
version: "1.0.0".into(),
effective_date: "2026-05-29".into(),
classification: "Confidential".into(),
status: Status::Draft,
logo: "logo.svg".into(),
commit: "abc123".into(),
content_hash: "sha256:deadbeef".into(),
generated_at: "2026-05-29".into(),
},
body_markdown: "# Intro\n\nHello.\n".into(),
sections: vec!["intro.md".into()],
}
}
#[test]
fn emits_imports_ctx_and_body() {
let out = emit(
&doc(),
EmitOptions {
toc: true,
page_numbers: true,
},
);
assert!(out.contains("#import \"theme.typ\""));
assert!(out.contains("#wisp-cover(ctx)"));
assert!(out.contains("#wisp-toc(ctx)"));
assert!(out.contains("= Intro"));
assert!(out.contains("Battle \\\"Creek\\\""));
assert!(out.contains("DRAFT"));
}
#[test]
fn omits_toc_and_footer_when_disabled() {
let out = emit(
&doc(),
EmitOptions {
toc: false,
page_numbers: false,
},
);
assert!(!out.contains("#wisp-toc(ctx)"));
assert!(!out.contains("footer: wisp-footer"));
}
}