use crate::ast::Node; /// Parse a template string into a list of AST nodes. pub fn parse(template: &str) -> Vec { let mut nodes = Vec::new(); let mut rest = template; while !rest.is_empty() { if let Some(pos) = rest.find("{{") { // Literal before the opening `{{`. if pos > 0 { nodes.push(Node::Literal(rest[..pos].to_string())); } let after_open = &rest[pos + 2..]; if let Some(close) = after_open.find("}}") { let expr = after_open[..close].trim(); rest = &after_open[close + 2..]; if let Some(each_path) = expr.strip_prefix("#each ") { // Block: {{#each path}} ... {{/each}} let path = parse_path(each_path.trim()); let (body, remaining) = parse_until_end_each(rest); nodes.push(Node::Each { path, body }); rest = remaining; } else if let Some(partial_expr) = expr.strip_prefix("> ") { // Partial: {{> name arg=value}} let (name, args) = parse_partial_expr(partial_expr.trim()); nodes.push(Node::Partial { name, args }); } else if !expr.starts_with('/') { // Binding: {{path.to.value}} let path = parse_path(expr); nodes.push(Node::Binding(path)); } // {{/each}} handled by parse_until_end_each } else { // No closing `}}` — treat rest as literal. nodes.push(Node::Literal(rest.to_string())); break; } } else { // No more `{{` — rest is literal. nodes.push(Node::Literal(rest.to_string())); break; } } nodes } fn parse_path(s: &str) -> Vec { s.split('.').map(|p| p.trim().to_string()).collect() } fn parse_until_end_each(input: &str) -> (Vec, &str) { // Find the matching {{/each}}. let mut depth = 1; let mut pos = 0; let bytes = input.as_bytes(); while pos < bytes.len() { if let Some(open) = input[pos..].find("{{") { let abs = pos + open; let after = &input[abs + 2..]; if let Some(close) = after.find("}}") { let expr = after[..close].trim(); if expr.starts_with("#each ") { depth += 1; } else if expr == "/each" { depth -= 1; if depth == 0 { let body_str = &input[..abs]; let rest = &after[close + 2..]; return (parse(body_str), rest); } } pos = abs + 2 + close + 2; } else { break; } } else { break; } } // Unmatched {{#each}} — return rest as literal body. (vec![Node::Literal(input.to_string())], "") } fn parse_partial_expr(expr: &str) -> (String, Vec<(String, Vec)>) { let parts: Vec<&str> = expr.splitn(2, ' ').collect(); let name = parts[0].to_string(); let mut args = Vec::new(); if parts.len() > 1 { for arg in parts[1].split_whitespace() { if let Some((key, val)) = arg.split_once('=') { args.push((key.to_string(), parse_path(val))); } } } (name, args) } #[cfg(test)] mod tests { use super::*; use crate::ast::Node; #[test] fn parse_literal() { let nodes = parse("

Hello

"); assert_eq!(nodes, vec![Node::Literal("

Hello

".into())]); } #[test] fn parse_binding() { let nodes = parse("{{article.title}}"); assert_eq!(nodes, vec![Node::Binding(vec!["article".into(), "title".into()])]); } #[test] fn parse_mixed() { let nodes = parse("

{{title}}

"); assert_eq!(nodes.len(), 3); assert_eq!(nodes[0], Node::Literal("

".into())); assert_eq!(nodes[1], Node::Binding(vec!["title".into()])); assert_eq!(nodes[2], Node::Literal("

".into())); } #[test] fn parse_each() { let nodes = parse("{{#each items}}
  • {{this}}
  • {{/each}}"); assert_eq!(nodes.len(), 1); match &nodes[0] { Node::Each { path, body } => { assert_eq!(path, &vec!["items".to_string()]); assert_eq!(body.len(), 3); } _ => panic!("expected Each"), } } #[test] fn parse_partial() { let nodes = parse("{{> article-card article=this}}"); assert_eq!(nodes.len(), 1); match &nodes[0] { Node::Partial { name, args } => { assert_eq!(name, "article-card"); assert_eq!(args.len(), 1); assert_eq!(args[0].0, "article"); assert_eq!(args[0].1, vec!["this".to_string()]); } _ => panic!("expected Partial"), } } #[test] fn parse_nested_each() { let nodes = parse("{{#each sections}}{{#each paragraphs}}{{this}}{{/each}}{{/each}}"); assert_eq!(nodes.len(), 1); match &nodes[0] { Node::Each { body, .. } => { assert_eq!(body.len(), 1); assert!(matches!(&body[0], Node::Each { .. })); } _ => panic!("expected nested Each"), } } }