use minijinja::{AutoEscape, Environment, UndefinedBehavior};
#[must_use]
pub fn build() -> Environment<'static> {
let mut env = Environment::new();
env.set_undefined_behavior(UndefinedBehavior::Strict);
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.set_keep_trailing_newline(true);
env.set_auto_escape_callback(|_name| AutoEscape::None);
#[cfg(feature = "json")]
env.add_filter("tojson", crate::filters::sorted_tojson);
env
}
#[cfg(test)]
mod tests {
use super::build;
#[test]
fn matches_canonical_settings() {
let env = build();
let trimmed = env
.template_from_str(" {% if true %}\nA\n{% endif %}\nB\n")
.expect("template compiles")
.render(())
.expect("template renders");
assert_eq!(trimmed, "A\nB\n", "trim_blocks/lstrip_blocks must be on");
let trailing = env
.template_from_str("line\n")
.expect("template compiles")
.render(())
.expect("template renders");
assert_eq!(trailing, "line\n", "keep_trailing_newline must be on");
let unescaped = env
.template_from_str("{{ value }}")
.expect("template compiles")
.render(minijinja::context! { value => "R&D <tag> 'quote'" })
.expect("template renders");
assert_eq!(unescaped, "R&D <tag> 'quote'", "autoescape must be off");
let error = env
.template_from_str("{{ missing }}")
.expect("template compiles")
.render(())
.expect_err("undefined variable must hard-fail, never render blank");
assert_eq!(
error.kind(),
minijinja::ErrorKind::UndefinedError,
"undefined variable must fail as UndefinedError: {error}"
);
}
#[cfg(feature = "json")]
#[test]
fn end_to_end_matches_canonical_corpus() {
use sha2::{Digest, Sha256};
fn sha256_hex(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{:02x}", *byte))
.collect()
}
let env = build();
let cases: [(&str, &str, &str, &str); 1] = [(
"handoff.md",
include_str!("../../../content/templates/handoff.md"),
r#"{"ARTIFACTS_COUNT": "3", "BRANCH": "v6.4.1-dev.0", "CARRY_FORWARDS": "- none", "COMMITS": "abc1234 first\ndef5678 second", "DATE": "2026-08-02", "DRIFT_RISK_COUNT": "0", "FILES_OF_INTEREST": "- crates/cli/", "LOCK_COUNT": "1", "MEM_COUNT": "2", "NEXT_FOCUS": "- verify release", "NORTH_STAR": "one canonical CLI", "OPEN_ISSUES_COUNT": "27", "SESSION": "sess-0001"}"#,
"51d55c5e3613860544124394b402166e91f5f98090db781273aebaafabfb2440",
)];
for (name, source, vars_json, expected_sha256) in cases {
let vars: serde_json::Value = serde_json::from_str(vars_json)
.unwrap_or_else(|e| panic!("{name}: vars parse: {e}"));
let rendered = env
.template_from_str(source)
.unwrap_or_else(|e| panic!("{name}: compile: {e}"))
.render(&vars)
.unwrap_or_else(|e| panic!("{name}: render: {e}"));
assert_eq!(
sha256_hex(rendered.as_bytes()),
expected_sha256,
"{name}: output diverged from the canonical content corpus"
);
}
}
}