1use anyhow::{anyhow, Result};
2use regex::Regex;
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7pub struct TemplateEngine {
9 variables: HashMap<String, serde_json::Value>,
11 template_dir: Option<String>,
13 left_delimiter: String,
15 right_delimiter: String,
17 for_left_delimiter: String,
19 for_right_delimiter: String,
21 preserve_loop_newlines: bool,
23 var_regex: Regex,
25 for_regex: Regex,
27 include_regex: Regex,
29}
30
31impl TemplateEngine {
32 pub fn new() -> Self {
34 Self::with_delimiters("{{", "}}")
35 }
36
37 pub fn with_delimiters(left: &str, right: &str) -> Self {
39 Self::with_all_delimiters(left, right, "{%", "%}")
40 }
41
42 pub fn with_all_delimiters(
44 var_left: &str,
45 var_right: &str,
46 for_left: &str,
47 for_right: &str,
48 ) -> Self {
49 let var_left_escaped = regex::escape(var_left);
50 let var_right_escaped = regex::escape(var_right);
51 let for_left_escaped = regex::escape(for_left);
52 let for_right_escaped = regex::escape(for_right);
53
54 let var_pattern = format!(
56 r"{}\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*{}",
57 var_left_escaped, var_right_escaped
58 );
59 let var_regex = Regex::new(&var_pattern).unwrap();
60
61 let for_pattern = format!(
64 "(?s){}\\s*for\\s+(\\w+)\\s+in\\s+(\\w+)(?:\\s+split\\s+\"([^\"]+)\")?\\s*{}(.*?){}\\s*endfor\\s*{}",
65 for_left_escaped, for_right_escaped, for_left_escaped, for_right_escaped
66 );
67 let for_regex = Regex::new(&for_pattern).unwrap();
68
69 let include_pattern = format!(
71 "{}\\s*include\\s+\"([^\"]+)\"\\s*{}",
72 for_left_escaped, for_right_escaped
73 );
74 let include_regex = Regex::new(&include_pattern).unwrap();
75
76 Self {
77 variables: HashMap::new(),
78 template_dir: None,
79 left_delimiter: var_left.to_string(),
80 right_delimiter: var_right.to_string(),
81 for_left_delimiter: for_left.to_string(),
82 for_right_delimiter: for_right.to_string(),
83 preserve_loop_newlines: true, var_regex,
85 for_regex,
86 include_regex,
87 }
88 }
89
90 pub fn set_template_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
92 self.template_dir = Some(path.as_ref().to_string_lossy().to_string());
93 self
94 }
95
96 pub fn set_variable<K: Into<String>, V: Into<serde_json::Value>>(
98 &mut self,
99 key: K,
100 value: V,
101 ) -> &mut Self {
102 self.variables.insert(key.into(), value.into());
103 self
104 }
105
106 pub fn set_variables(&mut self, vars: HashMap<String, serde_json::Value>) -> &mut Self {
108 for (key, value) in vars {
109 self.variables.insert(key, value);
110 }
111 self
112 }
113
114 pub fn set_preserve_loop_newlines(&mut self, preserve: bool) -> &mut Self {
116 self.preserve_loop_newlines = preserve;
117 self
118 }
119
120 pub fn render_string(&self, template: &str) -> Result<String> {
122 let mut result = template.to_string();
123
124 result = self.process_includes(&result)?;
126
127 result = self.process_for_loops(&result)?;
129
130 result = self.process_variables(&result)?;
132
133 Ok(result)
134 }
135
136 pub fn render_file<P: AsRef<Path>>(&self, template_path: P) -> Result<String> {
138 let template_content = fs::read_to_string(template_path)?;
139 self.render_string(&template_content)
140 }
141
142 fn process_includes(&self, template: &str) -> Result<String> {
144 let mut result = template.to_string();
145
146 while let Some(captures) = self.include_regex.captures(&result) {
147 let full_match = captures.get(0).unwrap().as_str();
148 let template_name = captures.get(1).unwrap().as_str();
149
150 let included_content = if let Some(ref dir) = self.template_dir {
151 let full_path = Path::new(dir).join(template_name);
152 fs::read_to_string(full_path)
153 .map_err(|e| anyhow!("Failed to include template '{}': {}", template_name, e))?
154 } else {
155 return Err(anyhow!(
156 "Template directory not set for include: {}",
157 template_name
158 ));
159 };
160
161 result = result.replace(full_match, &included_content);
162 }
163
164 Ok(result)
165 }
166
167 fn process_for_loops(&self, template: &str) -> Result<String> {
169 let mut result = template.to_string();
170
171 while let Some(captures) = self.for_regex.captures(&result) {
172 let full_match = captures.get(0).unwrap().as_str();
173 let item_name = captures.get(1).unwrap().as_str();
174 let array_name = captures.get(2).unwrap().as_str();
175 let split_delimiter = captures.get(3).map(|m| m.as_str());
176 let loop_content = captures.get(4).unwrap().as_str();
177
178 let array_value = self
179 .variables
180 .get(array_name)
181 .ok_or_else(|| anyhow!("Array '{}' not found in variables", array_name))?;
182
183 let items: Vec<serde_json::Value> = if let Some(delimiter) = split_delimiter {
185 match array_value {
187 serde_json::Value::String(s) => {
188 s.split(delimiter)
189 .map(|part| serde_json::Value::String(part.to_string()))
190 .collect()
191 }
192 _ => {
193 return Err(anyhow!(
194 "Cannot split non-string variable '{}'",
195 array_name
196 ))
197 }
198 }
199 } else {
200 if let serde_json::Value::Array(items) = array_value {
202 items.clone()
203 } else {
204 return Err(anyhow!("'{}' is not an array", array_name));
205 }
206 };
207
208 let mut loop_result = String::new();
209
210 for item in items {
211 let mut temp_vars = self.variables.clone();
212 temp_vars.insert(item_name.to_string(), item.clone());
213
214 let temp_engine = Self {
215 variables: temp_vars,
216 template_dir: self.template_dir.clone(),
217 left_delimiter: self.left_delimiter.clone(),
218 right_delimiter: self.right_delimiter.clone(),
219 for_left_delimiter: self.for_left_delimiter.clone(),
220 for_right_delimiter: self.for_right_delimiter.clone(),
221 preserve_loop_newlines: self.preserve_loop_newlines,
222 var_regex: self.var_regex.clone(),
223 for_regex: self.for_regex.clone(),
224 include_regex: self.include_regex.clone(),
225 };
226
227 let mut rendered = temp_engine.process_variables(loop_content)?;
228
229 if !self.preserve_loop_newlines {
231 let lines: Vec<&str> = rendered
233 .lines()
234 .filter(|line| !line.trim().is_empty())
235 .collect();
236
237 if !lines.is_empty() {
239 rendered = lines.join("\n");
240
241 if !loop_result.is_empty() {
243 loop_result.push_str("\n");
244 }
245 } else {
246 rendered = String::new();
247 }
248 }
249
250 loop_result.push_str(&rendered);
251 }
252
253 result = result.replace(full_match, &loop_result);
254 }
255
256 Ok(result)
257 }
258
259 fn process_variables(&self, template: &str) -> Result<String> {
261 let mut result = template.to_string();
262
263 while let Some(captures) = self.var_regex.captures(&result) {
264 let full_match = captures.get(0).unwrap().as_str();
265 let variable_path = captures.get(1).unwrap().as_str();
266
267 let value = self.get_variable_value(variable_path)?;
268 let value_str = match value {
269 serde_json::Value::String(s) => s.clone(),
270 v => v.to_string(),
271 };
272
273 result = result.replace(full_match, &value_str);
274 }
275
276 Ok(result)
277 }
278
279 fn get_variable_value(&self, path: &str) -> Result<serde_json::Value> {
281 let parts: Vec<&str> = path.split('.').collect();
282
283 if parts.is_empty() {
284 return Err(anyhow!("Empty variable path"));
285 }
286
287 let current = self
288 .variables
289 .get(parts[0])
290 .ok_or_else(|| anyhow!("Variable '{}' not found", parts[0]))?;
291
292 if parts.len() == 1 {
293 return Ok(current.clone());
294 }
295
296 let mut result = current;
297 for part in &parts[1..] {
298 match result {
299 serde_json::Value::Object(map) => {
300 result = map
301 .get(*part)
302 .ok_or_else(|| anyhow!("Property '{}' not found in variable", part))?;
303 }
304 _ => {
305 return Err(anyhow!(
306 "Cannot access property '{}' on non-object value",
307 part
308 ))
309 }
310 }
311 }
312
313 Ok(result.clone())
314 }
315}
316
317impl Default for TemplateEngine {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use serde_json::json;
327 use tracing::instrument::WithSubscriber;
328
329 #[test]
330 fn test_variable_replacement() {
331 let mut engine = TemplateEngine::new();
332 engine.set_variable("name", "World");
333 engine.set_variable("age", 25);
334
335 let result = engine
336 .render_string("Hello, {{ name }}! You are {{ age }} years old.")
337 .unwrap();
338 assert_eq!(result, "Hello, World! You are 25 years old.");
339 }
340
341 #[test]
342 fn test_nested_variable_access() {
343 let mut engine = TemplateEngine::new();
344 engine.set_variable(
345 "user",
346 json!({
347 "name": "Alice",
348 "profile": {
349 "age": 30,
350 "city": "Beijing"
351 }
352 }),
353 );
354
355 let result = engine
356 .render_string("Name: {{ user.name }}, City: {{ user.profile.city }}")
357 .unwrap();
358 assert_eq!(result, "Name: Alice, City: Beijing");
359 }
360
361 #[test]
362 fn test_for_loop() {
363 let mut engine = TemplateEngine::with_all_delimiters("{{", "}}", "#{%", "%}");
364 engine.set_variable("items", json!(["apple", "banana", "cherry"]));
365
366 let template = r#"
367#{% for item in items %}
368 - {{ item }}
369#{% endfor %}"#;
370
371 let result = engine
372 .set_preserve_loop_newlines(false)
373 .render_string(template)
374 .unwrap();
375 let expected = r#"
376 - apple
377 - banana
378 - cherry"#;
379 assert_eq!(result, expected);
380 }
381
382 #[test]
383 fn test_custom_delimiters() {
384 let mut engine = TemplateEngine::with_delimiters("${", "}");
385 engine.set_variable("name", "Custom");
386
387 let result = engine.render_string("Hello, ${ name }!").unwrap();
388 assert_eq!(result, "Hello, Custom!");
389 }
390
391 #[test]
392 fn test_complex_template() {
393 let mut engine = TemplateEngine::new();
394 engine.set_variable("title", "User List");
395 engine.set_variable(
396 "users",
397 json!( [
398 {"name": "Alice", "age": 25},
399 {"name": "Bob", "age": 30},
400 {"name": "Charlie", "age": 35}
401 ] ),
402 );
403
404 let template = r#"
405 <h1>{{ title }}</h1>
406 <ul>
407 {% for user in users %}
408 <li>{{ user.name }} ({{ user.age }} years old)</li>
409 {% endfor %}
410 </ul>"#;
411
412 let result = engine.render_string(template).unwrap();
413
414 assert!(result.contains("<h1>User List</h1>"));
415 assert!(result.contains("<li>Alice (25 years old)</li>"));
416 assert!(result.contains("<li>Bob (30 years old)</li>"));
417 assert!(result.contains("<li>Charlie (35 years old)</li>"));
418 }
419
420 #[test]
421 fn test_custom_for_tags() {
422 let mut engine = TemplateEngine::with_all_delimiters("{{", "}}", "<%", "%>");
423 engine.set_variable("items", json!(["red", "green", "blue"]));
424
425 let template = r#"
426<% for color in items %>
427* {{ color }}
428<% endfor %>"#;
429
430 let result = engine.render_string(template).unwrap();
431
432 assert!(result.contains("* red"));
433 assert!(result.contains("* green"));
434 assert!(result.contains("* blue"));
435 }
436
437 #[test]
438 fn test_preserve_loop_newlines() {
439 let mut engine = TemplateEngine::new();
441 engine.set_variable("items", json!(["a", "b", "c"]));
442
443 let template = r#"
444{% for item in items %}
445- {{ item }}
446{% endfor %}"#;
447
448 let result = engine.render_string(template).unwrap();
449
450 assert!(result.contains("\n- a\n"));
452 assert!(result.contains("\n- b\n"));
453 assert!(result.contains("\n- c\n"));
454
455 engine.set_preserve_loop_newlines(false);
457 let result = engine.render_string(template).unwrap();
458
459 assert!(result.contains("- a\n- b\n- c"));
461 }
462
463 #[test]
464 fn test_split_functionality() {
465 let mut engine = TemplateEngine::new();
466 engine.set_variable("csv_string", "apple,banana,cherry");
467
468 let template = r#"
469{% for fruit in csv_string split "," %}
470- {{ fruit }}
471{% endfor %}"#;
472
473 let result = engine
474 .set_preserve_loop_newlines(false)
475 .render_string(template)
476 .unwrap();
477
478 let expected = r#"
479- apple
480- banana
481- cherry"#;
482 assert_eq!(result, expected);
483 }
484
485 #[test]
486 fn test_split_with_space_delimiter() {
487 let mut engine = TemplateEngine::new();
488 engine.set_variable("space_separated", "red green blue");
489
490 let template = r#"
491{% for color in space_separated split " " %}
492* {{ color }}
493{% endfor %}"#;
494
495 let result = engine
496 .set_preserve_loop_newlines(false)
497 .render_string(template)
498 .unwrap();
499
500 assert!(result.contains("* red"));
501 assert!(result.contains("* green"));
502 assert!(result.contains("* blue"));
503 }
504
505 #[test]
506 fn test_split_with_complex_delimiter() {
507 let mut engine = TemplateEngine::new();
508 engine.set_variable("complex_string", "item1||item2||item3");
509
510 let template = r#"
511{% for item in complex_string split "||" %}
512{{ item }}
513{% endfor %}"#;
514
515 let result = engine
516 .set_preserve_loop_newlines(false)
517 .render_string(template)
518 .unwrap();
519
520 assert!(result.contains("item1"));
521 assert!(result.contains("item2"));
522 assert!(result.contains("item3"));
523 }
524}