use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};
use serde_json::Value;
pub fn register_all(hbs: &mut Handlebars<'static>) {
hbs.register_helper("default", Box::new(default_helper));
hbs.register_helper("eq", Box::new(eq_helper));
hbs.register_helper("json_str", Box::new(json_str_helper));
hbs.register_helper("json_value", Box::new(json_value_helper));
hbs.register_helper("has_any", Box::new(has_any_helper));
hbs.register_helper("concat", Box::new(concat_helper));
hbs.register_helper("format_number", Box::new(format_number_helper));
hbs.register_helper("join", Box::new(join_helper));
hbs.register_helper("theme", Box::new(theme_helper));
}
fn default_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let val = h.param(0).and_then(|p| p.value().as_str());
let fallback = h.param(1).and_then(|p| p.value().as_str()).unwrap_or("");
out.write(match val {
Some(v) if !v.is_empty() => v,
_ => fallback,
})?;
Ok(())
}
fn eq_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let a = h.param(0).map(|p| p.value());
let b = h.param(1).map(|p| p.value());
let result = matches!((a, b), (Some(a), Some(b)) if a == b);
out.write(if result { "true" } else { "" })?;
Ok(())
}
fn json_str_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let val = h.param(0).map(|p| p.value()).unwrap_or(&Value::Null);
let s = match val {
Value::String(s) => s.clone(),
Value::Null => String::new(),
other => other.to_string(),
};
let escaped = serde_json::to_string(&s).unwrap_or_else(|_| "\"\"".to_string());
out.write(&escaped)?;
Ok(())
}
fn json_value_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let val = h.param(0).map(|p| p.value().clone()).unwrap_or(Value::Null);
let serialized = serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string());
out.write(&serialized)?;
Ok(())
}
fn has_any_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let val = h.param(0).map(|p| p.value()).unwrap_or(&Value::Null);
let has_any = match val {
Value::Array(arr) => !arr.is_empty(),
Value::Object(obj) => !obj.is_empty(),
Value::String(s) => !s.is_empty(),
Value::Null => false,
_ => true,
};
out.write(if has_any { "true" } else { "" })?;
Ok(())
}
fn concat_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let mut result = String::new();
for p in h.params() {
match p.value() {
Value::String(s) => result.push_str(s),
other => result.push_str(&other.to_string()),
}
}
out.write(&result)?;
Ok(())
}
fn format_number_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let value = h.param(0).and_then(|p| p.value().as_f64()).unwrap_or(0.0);
let decimals = h.param(1).and_then(|p| p.value().as_u64()).unwrap_or(2) as usize;
out.write(&format!("{:.*}", decimals, value))?;
Ok(())
}
fn join_helper(
h: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let arr = h.param(0).and_then(|p| p.value().as_array());
let sep = h.param(1).and_then(|p| p.value().as_str()).unwrap_or(", ");
if let Some(arr) = arr {
let parts: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
out.write(&parts.join(sep))?;
}
Ok(())
}
fn theme_helper(
h: &Helper,
_: &Handlebars,
ctx: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
let key = h.param(0).and_then(|p| p.value().as_str()).unwrap_or("");
let fallback = h.param(1).and_then(|p| p.value().as_str()).unwrap_or("");
let data = ctx.data();
let token_value = data
.get("theme")
.and_then(|t| t.get("tokens"))
.and_then(|tokens| tokens.get(key));
match token_value {
Some(Value::String(s)) => out.write(s)?,
Some(other) => out.write(&other.to_string())?,
None => out.write(fallback)?,
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn render(template: &str, data: Value) -> String {
let mut hbs = Handlebars::new();
register_all(&mut hbs);
hbs.render_template(template, &data).unwrap()
}
#[test]
fn test_default_with_value() {
let result = render(
r#"{{default name "Guest"}}"#,
serde_json::json!({"name": "Alice"}),
);
assert_eq!(result, "Alice");
}
#[test]
fn test_default_fallback() {
let result = render(r#"{{default name "Guest"}}"#, serde_json::json!({}));
assert_eq!(result, "Guest");
}
#[test]
fn test_json_str_escapes_quotes() {
let result = render(
r#"{{json_str msg}}"#,
serde_json::json!({"msg": "he said \"hi\""}),
);
assert_eq!(result, r#""he said \"hi\"""#);
}
#[test]
fn test_json_str_unicode() {
let result = render(r#"{{json_str emoji}}"#, serde_json::json!({"emoji": "✈️"}));
assert!(result.starts_with('"') && result.ends_with('"'));
}
#[test]
fn test_eq_true() {
let result = render(
r#"{{#if (eq status "active")}}YES{{/if}}"#,
serde_json::json!({"status": "active"}),
);
assert_eq!(result, "YES");
}
#[test]
fn test_has_any_empty_array() {
let result = render(
r#"{{#if (has_any items)}}HAS{{else}}EMPTY{{/if}}"#,
serde_json::json!({"items": []}),
);
assert_eq!(result, "EMPTY");
}
#[test]
fn test_has_any_with_items() {
let result = render(
r#"{{#if (has_any items)}}HAS{{/if}}"#,
serde_json::json!({"items": [1, 2]}),
);
assert_eq!(result, "HAS");
}
#[test]
fn test_concat() {
let result = render(
r#"{{concat "Hi " name "!"}}"#,
serde_json::json!({"name": "Bob"}),
);
assert_eq!(result, "Hi Bob!");
}
#[test]
fn test_format_number() {
let result = render(
r#"{{format_number price 2}}"#,
serde_json::json!({"price": 12.3456}),
);
assert_eq!(result, "12.35");
}
#[test]
fn test_join() {
let result = render(
r#"{{join tags ", "}}"#,
serde_json::json!({"tags": ["a", "b", "c"]}),
);
assert_eq!(result, "a, b, c");
}
#[test]
fn test_theme_token() {
let result = render(
r##"{{theme "color_primary" "#000"}}"##,
serde_json::json!({"theme": {"tokens": {"color_primary": "#0d9488"}}}),
);
assert_eq!(result, "#0d9488");
}
#[test]
fn test_theme_fallback() {
let result = render(
r##"{{theme "missing" "#000"}}"##,
serde_json::json!({"theme": {"tokens": {}}}),
);
assert_eq!(result, "#000");
}
}