use std::collections::BTreeMap; use crate::ast::Node; use crate::value::Value; /// Render a list of AST nodes against a context value. /// /// `partials` maps partial names to their parsed node lists. pub fn render( nodes: &[Node], context: &Value, partials: &std::collections::HashMap>, ) -> String { let mut output = String::new(); for node in nodes { match node { Node::Literal(text) => { output.push_str(text); } Node::Binding(path) => { if path.len() == 1 && path[0] == "this" { output.push_str(&context.to_display()); } else { let val = context.resolve(path); output.push_str(&val.to_display()); } } Node::Each { path, body } => { let list_val = if path.len() == 1 && path[0] == "this" { context } else { context.resolve(path) }; for item in list_val.as_list() { output.push_str(&render(body, item, partials)); } } Node::Partial { name, args } => { if let Some(partial_nodes) = partials.get(name.as_str()) { // Build the partial context from args. let partial_ctx = if args.is_empty() { context.clone() } else { let mut map = BTreeMap::new(); for (key, path) in args { let val = if path.len() == 1 && path[0] == "this" { context.clone() } else { context.resolve(path).clone() }; map.insert(key.clone(), val); } Value::Object(map) }; output.push_str(&render(partial_nodes, &partial_ctx, partials)); } } } } output } #[cfg(test)] mod tests { use super::*; use crate::parser; use std::collections::HashMap; fn ctx(json: serde_json::Value) -> Value { Value::from_json(&json) } #[test] fn render_literal() { let nodes = parser::parse("

Hello

"); let result = render(&nodes, &Value::Null, &HashMap::new()); assert_eq!(result, "

Hello

"); } #[test] fn render_binding() { let nodes = parser::parse("

{{title}}

"); let context = ctx(serde_json::json!({"title": "My Article"})); let result = render(&nodes, &context, &HashMap::new()); assert_eq!(result, "

My Article

"); } #[test] fn render_nested_binding() { let nodes = parser::parse("by {{article.author}}"); let context = ctx(serde_json::json!({"article": {"author": "Shoney"}})); let result = render(&nodes, &context, &HashMap::new()); assert_eq!(result, "by Shoney"); } #[test] fn render_each() { let nodes = parser::parse(""); let context = ctx(serde_json::json!({"tags": ["rust", "linux"]})); let result = render(&nodes, &context, &HashMap::new()); assert_eq!(result, ""); } #[test] fn render_each_objects() { let nodes = parser::parse("{{#each sections}}

{{heading}}

{{/each}}"); let context = ctx(serde_json::json!({ "sections": [ {"heading": "Intro"}, {"heading": "Details"} ] })); let result = render(&nodes, &context, &HashMap::new()); assert_eq!(result, "

Intro

Details

"); } #[test] fn render_partial() { let nodes = parser::parse("{{> card item=this}}"); let mut partials = HashMap::new(); partials.insert( "card".to_string(), parser::parse("
{{item.name}}
"), ); let context = ctx(serde_json::json!({"name": "Test"})); let result = render(&nodes, &context, &partials); assert_eq!(result, "
Test
"); } #[test] fn render_missing_binding() { let nodes = parser::parse("{{nonexistent}}"); let context = ctx(serde_json::json!({})); let result = render(&nodes, &context, &HashMap::new()); assert_eq!(result, ""); } }