use anyhow::{anyhow, Result};
use std::collections::HashMap;
pub fn render(template: &str, vars: &HashMap<String, String>) -> Result<String> {
let mut out = String::with_capacity(template.len());
let mut rest = template;
let mut keep_stack: Vec<bool> = Vec::new();
while let Some(start) = rest.find("{{") {
let active = keep_stack.iter().all(|b| *b);
if active {
out.push_str(&rest[..start]);
}
let after_open = &rest[start + 2..];
let end = after_open
.find("}}")
.ok_or_else(|| anyhow!("Unclosed placeholder in template"))?;
let token = after_open[..end].trim();
rest = &after_open[end + 2..];
if let Some(var) = token.strip_prefix("#if ") {
let truthy = is_truthy(vars, var.trim());
keep_stack.push(truthy);
} else if let Some(var) = token.strip_prefix("#unless ") {
let truthy = is_truthy(vars, var.trim());
keep_stack.push(!truthy);
} else if token == "/if" || token == "/unless" {
keep_stack
.pop()
.ok_or_else(|| anyhow!("Unmatched {{{{/if}}}} or {{{{/unless}}}}"))?;
} else if active {
let value = vars
.get(token)
.ok_or_else(|| anyhow!("Unknown template variable: {}", token))?;
out.push_str(value);
}
}
if !keep_stack.is_empty() {
return Err(anyhow!("Unclosed {{{{#if}}}} or {{{{#unless}}}} block"));
}
out.push_str(rest);
Ok(out)
}
fn is_truthy(vars: &HashMap<String, String>, key: &str) -> bool {
vars.get(key).map(|v| !v.is_empty()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn vars(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn renders_simple() {
let v = vars(&[("name", "alice")]);
assert_eq!(render("hello {{ name }}", &v).unwrap(), "hello alice");
}
#[test]
fn renders_no_whitespace() {
let v = vars(&[("name", "bob")]);
assert_eq!(render("hi {{name}}!", &v).unwrap(), "hi bob!");
}
#[test]
fn fails_on_unknown_var() {
let v = vars(&[]);
assert!(render("{{ missing }}", &v).is_err());
}
#[test]
fn if_truthy_includes_block() {
let v = vars(&[("flag", "1")]);
assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a YES b");
}
#[test]
fn if_falsy_skips_block() {
let v = vars(&[("flag", "")]);
assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a b");
}
#[test]
fn if_missing_var_is_falsy() {
let v = vars(&[]);
assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a b");
}
#[test]
fn unless_negates() {
let v = vars(&[("flag", "")]);
assert_eq!(
render("a {{#unless flag}}NO{{/unless}} b", &v).unwrap(),
"a NO b"
);
let v2 = vars(&[("flag", "1")]);
assert_eq!(
render("a {{#unless flag}}NO{{/unless}} b", &v2).unwrap(),
"a b"
);
}
#[test]
fn nested_blocks() {
let v = vars(&[("a", "1"), ("b", "1")]);
assert_eq!(
render("{{#if a}}A{{#if b}}B{{/if}}C{{/if}}", &v).unwrap(),
"ABC"
);
let v2 = vars(&[("a", "1"), ("b", "")]);
assert_eq!(
render("{{#if a}}A{{#if b}}B{{/if}}C{{/if}}", &v2).unwrap(),
"AC"
);
}
#[test]
fn skipped_block_tolerates_unknown_var() {
let v = vars(&[("flag", "")]);
assert_eq!(
render("{{#if flag}}{{ never_resolved }}{{/if}}", &v).unwrap(),
""
);
}
#[test]
fn active_block_still_errors_on_unknown_var() {
let v = vars(&[("flag", "1")]);
assert!(render("{{#if flag}}{{ missing }}{{/if}}", &v).is_err());
}
#[test]
fn unmatched_close_errors() {
let v = vars(&[]);
assert!(render("hello {{/if}}", &v).is_err());
}
#[test]
fn unclosed_block_errors() {
let v = vars(&[("flag", "1")]);
assert!(render("{{#if flag}}oops", &v).is_err());
}
}