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!(
65 "(?s){}\\s*for\\s+(\\w+)\\s+in\\s+(\\w+)(?:\\s+(split|jsonparse)(?:\\s+\"([^\"]+)\")?)?\\s*{}(.*?){}\\s*endfor\\s*{}",
66 for_left_escaped, for_right_escaped, for_left_escaped, for_right_escaped
67 );
68 let for_regex = Regex::new(&for_pattern).unwrap();
69
70 let include_pattern = format!(
72 "{}\\s*include\\s+\"([^\"]+)\"\\s*{}",
73 for_left_escaped, for_right_escaped
74 );
75 let include_regex = Regex::new(&include_pattern).unwrap();
76
77 Self {
78 variables: HashMap::new(),
79 template_dir: None,
80 left_delimiter: var_left.to_string(),
81 right_delimiter: var_right.to_string(),
82 for_left_delimiter: for_left.to_string(),
83 for_right_delimiter: for_right.to_string(),
84 preserve_loop_newlines: true, var_regex,
86 for_regex,
87 include_regex,
88 }
89 }
90
91 pub fn set_template_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
93 self.template_dir = Some(path.as_ref().to_string_lossy().to_string());
94 self
95 }
96
97 pub fn set_variable<K: Into<String>, V: Into<serde_json::Value>>(
99 &mut self,
100 key: K,
101 value: V,
102 ) -> &mut Self {
103 self.variables.insert(key.into(), value.into());
104 self
105 }
106
107 pub fn set_variables(&mut self, vars: HashMap<String, serde_json::Value>) -> &mut Self {
109 for (key, value) in vars {
110 self.variables.insert(key, value);
111 }
112 self
113 }
114
115 pub fn set_preserve_loop_newlines(&mut self, preserve: bool) -> &mut Self {
117 self.preserve_loop_newlines = preserve;
118 self
119 }
120
121 pub fn render_string(&self, template: &str) -> Result<String> {
123 let mut result = template.to_string();
124
125 result = self.process_includes(&result)?;
127
128 result = self.process_for_loops(&result)?;
130
131 result = self.process_variables(&result)?;
133
134 Ok(result)
135 }
136
137 pub fn render_file<P: AsRef<Path>>(&self, template_path: P) -> Result<String> {
139 let template_content = fs::read_to_string(template_path)?;
140 self.render_string(&template_content)
141 }
142
143 fn process_includes(&self, template: &str) -> Result<String> {
145 let mut result = template.to_string();
146
147 while let Some(captures) = self.include_regex.captures(&result) {
148 let full_match = captures.get(0).unwrap().as_str();
149 let template_name = captures.get(1).unwrap().as_str();
150
151 let included_content = if let Some(ref dir) = self.template_dir {
152 let full_path = Path::new(dir).join(template_name);
153 fs::read_to_string(full_path)
154 .map_err(|e| anyhow!("Failed to include template '{}': {}", template_name, e))?
155 } else {
156 return Err(anyhow!(
157 "Template directory not set for include: {}",
158 template_name
159 ));
160 };
161
162 result = result.replace(full_match, &included_content);
163 }
164
165 Ok(result)
166 }
167
168 fn process_for_loops(&self, template: &str) -> Result<String> {
170 let mut result = template.to_string();
171
172 while let Some(captures) = self.for_regex.captures(&result) {
173 let full_match = captures.get(0).unwrap().as_str();
174 let item_name = captures.get(1).unwrap().as_str();
175 let array_name = captures.get(2).unwrap().as_str();
176 let operation = captures.get(3).map(|m| m.as_str());
177 let operation_param = captures.get(4).map(|m| m.as_str());
178 let loop_content = captures.get(5).unwrap().as_str();
179
180 let array_value = self
181 .variables
182 .get(array_name)
183 .ok_or_else(|| anyhow!("Array '{}' not found in variables", array_name))?;
184
185 let items: Vec<serde_json::Value> = match operation {
187 Some("split") => {
188 let delimiter = operation_param.ok_or_else(|| anyhow!("Split operation requires a delimiter"))?;
190 match array_value {
191 serde_json::Value::String(s) => {
192 s.split(delimiter)
193 .map(|part| serde_json::Value::String(part.to_string()))
194 .collect()
195 }
196 _ => {
197 return Err(anyhow!(
198 "Cannot split non-string variable '{}'",
199 array_name
200 ))
201 }
202 }
203 }
204 Some("jsonparse") => {
205 match array_value {
207 serde_json::Value::String(s) => {
208 let parsed: serde_json::Value = serde_json::from_str(s)
209 .map_err(|e| anyhow!("Failed to parse JSON from variable '{}': {}", array_name, e))?;
210
211 match parsed {
212 serde_json::Value::Array(arr) => arr,
213 serde_json::Value::Object(obj) => {
214 obj.into_iter()
216 .map(|(k, v)| {
217 serde_json::json!({
218 "key": k,
219 "value": v
220 })
221 })
222 .collect()
223 }
224 _ => {
225 return Err(anyhow!(
226 "JSON must be an array or object for iteration, got: {}",
227 parsed
228 ))
229 }
230 }
231 }
232 _ => {
233 return Err(anyhow!(
234 "Cannot jsonparse non-string variable '{}'",
235 array_name
236 ))
237 }
238 }
239 }
240 None => {
241 if let serde_json::Value::Array(items) = array_value {
243 items.clone()
244 } else {
245 return Err(anyhow!("'{}' is not an array", array_name));
246 }
247 }
248 _ => {
249 return Err(anyhow!("Unknown operation: {}", operation.unwrap()));
250 }
251 };
252
253 let mut loop_result = String::new();
254
255 for item in items {
256 let mut temp_vars = self.variables.clone();
257 temp_vars.insert(item_name.to_string(), item.clone());
258
259 let temp_engine = Self {
260 variables: temp_vars,
261 template_dir: self.template_dir.clone(),
262 left_delimiter: self.left_delimiter.clone(),
263 right_delimiter: self.right_delimiter.clone(),
264 for_left_delimiter: self.for_left_delimiter.clone(),
265 for_right_delimiter: self.for_right_delimiter.clone(),
266 preserve_loop_newlines: self.preserve_loop_newlines,
267 var_regex: self.var_regex.clone(),
268 for_regex: self.for_regex.clone(),
269 include_regex: self.include_regex.clone(),
270 };
271
272 let mut rendered = temp_engine.process_variables(loop_content)?;
273
274 if !self.preserve_loop_newlines {
276 let lines: Vec<&str> = rendered
278 .lines()
279 .filter(|line| !line.trim().is_empty())
280 .collect();
281
282 if !lines.is_empty() {
284 rendered = lines.join("\n");
285
286 if !loop_result.is_empty() {
288 loop_result.push_str("\n");
289 }
290 } else {
291 rendered = String::new();
292 }
293 }
294
295 loop_result.push_str(&rendered);
296 }
297
298 result = result.replace(full_match, &loop_result);
299 }
300
301 Ok(result)
302 }
303
304 fn process_variables(&self, template: &str) -> Result<String> {
306 let mut result = template.to_string();
307
308 while let Some(captures) = self.var_regex.captures(&result) {
309 let full_match = captures.get(0).unwrap().as_str();
310 let variable_path = captures.get(1).unwrap().as_str();
311
312 let value = self.get_variable_value(variable_path)?;
313 let value_str = match value {
314 serde_json::Value::String(s) => s.clone(),
315 v => v.to_string(),
316 };
317
318 result = result.replace(full_match, &value_str);
319 }
320
321 Ok(result)
322 }
323
324 fn get_variable_value(&self, path: &str) -> Result<serde_json::Value> {
326 let parts: Vec<&str> = path.split('.').collect();
327
328 if parts.is_empty() {
329 return Err(anyhow!("Empty variable path"));
330 }
331
332 let current = self
333 .variables
334 .get(parts[0])
335 .ok_or_else(|| anyhow!("Variable '{}' not found", parts[0]))?;
336
337 if parts.len() == 1 {
338 return Ok(current.clone());
339 }
340
341 let mut result = current;
342 for part in &parts[1..] {
343 match result {
344 serde_json::Value::Object(map) => {
345 result = map
346 .get(*part)
347 .ok_or_else(|| anyhow!("Property '{}' not found in variable", part))?;
348 }
349 _ => {
350 return Err(anyhow!(
351 "Cannot access property '{}' on non-object value",
352 part
353 ))
354 }
355 }
356 }
357
358 Ok(result.clone())
359 }
360}
361
362impl Default for TemplateEngine {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use serde_json::json;
372 use tracing::instrument::WithSubscriber;
373
374 #[test]
375 fn test_variable_replacement() {
376 let mut engine = TemplateEngine::new();
377 engine.set_variable("name", "World");
378 engine.set_variable("age", 25);
379
380 let result = engine
381 .render_string("Hello, {{ name }}! You are {{ age }} years old.")
382 .unwrap();
383 assert_eq!(result, "Hello, World! You are 25 years old.");
384 }
385
386 #[test]
387 fn test_nested_variable_access() {
388 let mut engine = TemplateEngine::new();
389 engine.set_variable(
390 "user",
391 json!({
392 "name": "Alice",
393 "profile": {
394 "age": 30,
395 "city": "Beijing"
396 }
397 }),
398 );
399
400 let result = engine
401 .render_string("Name: {{ user.name }}, City: {{ user.profile.city }}")
402 .unwrap();
403 assert_eq!(result, "Name: Alice, City: Beijing");
404 }
405
406 #[test]
407 fn test_for_loop() {
408 let mut engine = TemplateEngine::with_all_delimiters("{{", "}}", "#{%", "%}");
409 engine.set_variable("items", json!(["apple", "banana", "cherry"]));
410
411 let template = r#"
412#{% for item in items %}
413 - {{ item }}
414#{% endfor %}"#;
415
416 let result = engine
417 .set_preserve_loop_newlines(false)
418 .render_string(template)
419 .unwrap();
420 let expected = r#"
421 - apple
422 - banana
423 - cherry"#;
424 assert_eq!(result, expected);
425 }
426
427 #[test]
428 fn test_custom_delimiters() {
429 let mut engine = TemplateEngine::with_delimiters("${", "}");
430 engine.set_variable("name", "Custom");
431
432 let result = engine.render_string("Hello, ${ name }!").unwrap();
433 assert_eq!(result, "Hello, Custom!");
434 }
435
436 #[test]
437 fn test_complex_template() {
438 let mut engine = TemplateEngine::new();
439 engine.set_variable("title", "User List");
440 engine.set_variable(
441 "users",
442 json!( [
443 {"name": "Alice", "age": 25},
444 {"name": "Bob", "age": 30},
445 {"name": "Charlie", "age": 35}
446 ] ),
447 );
448
449 let template = r#"
450 <h1>{{ title }}</h1>
451 <ul>
452 {% for user in users %}
453 <li>{{ user.name }} ({{ user.age }} years old)</li>
454 {% endfor %}
455 </ul>"#;
456
457 let result = engine.render_string(template).unwrap();
458
459 assert!(result.contains("<h1>User List</h1>"));
460 assert!(result.contains("<li>Alice (25 years old)</li>"));
461 assert!(result.contains("<li>Bob (30 years old)</li>"));
462 assert!(result.contains("<li>Charlie (35 years old)</li>"));
463 }
464
465 #[test]
466 fn test_custom_for_tags() {
467 let mut engine = TemplateEngine::with_all_delimiters("{{", "}}", "<%", "%>");
468 engine.set_variable("items", json!(["red", "green", "blue"]));
469
470 let template = r#"
471<% for color in items %>
472* {{ color }}
473<% endfor %>"#;
474
475 let result = engine.render_string(template).unwrap();
476
477 assert!(result.contains("* red"));
478 assert!(result.contains("* green"));
479 assert!(result.contains("* blue"));
480 }
481
482 #[test]
483 fn test_preserve_loop_newlines() {
484 let mut engine = TemplateEngine::new();
486 engine.set_variable("items", json!(["a", "b", "c"]));
487
488 let template = r#"
489{% for item in items %}
490- {{ item }}
491{% endfor %}"#;
492
493 let result = engine.render_string(template).unwrap();
494
495 assert!(result.contains("\n- a\n"));
497 assert!(result.contains("\n- b\n"));
498 assert!(result.contains("\n- c\n"));
499
500 engine.set_preserve_loop_newlines(false);
502 let result = engine.render_string(template).unwrap();
503
504 assert!(result.contains("- a\n- b\n- c"));
506 }
507
508 #[test]
509 fn test_split_functionality() {
510 let mut engine = TemplateEngine::new();
511 engine.set_variable("csv_string", "apple,banana,cherry");
512
513 let template = r#"
514{% for fruit in csv_string split "," %}
515- {{ fruit }}
516{% endfor %}"#;
517
518 let result = engine
519 .set_preserve_loop_newlines(false)
520 .render_string(template)
521 .unwrap();
522
523 let expected = r#"
524- apple
525- banana
526- cherry"#;
527 assert_eq!(result, expected);
528 }
529
530 #[test]
531 fn test_split_with_space_delimiter() {
532 let mut engine = TemplateEngine::new();
533 engine.set_variable("space_separated", "red green blue");
534
535 let template = r#"
536{% for color in space_separated split " " %}
537* {{ color }}
538{% endfor %}"#;
539
540 let result = engine
541 .set_preserve_loop_newlines(false)
542 .render_string(template)
543 .unwrap();
544
545 assert!(result.contains("* red"));
546 assert!(result.contains("* green"));
547 assert!(result.contains("* blue"));
548 }
549
550 #[test]
551 fn test_split_with_complex_delimiter() {
552 let mut engine = TemplateEngine::new();
553 engine.set_variable("complex_string", "item1||item2||item3");
554
555 let template = r#"
556{% for item in complex_string split "||" %}
557{{ item }}
558{% endfor %}"#;
559
560 let result = engine
561 .set_preserve_loop_newlines(false)
562 .render_string(template)
563 .unwrap();
564
565 assert!(result.contains("item1"));
566 assert!(result.contains("item2"));
567 assert!(result.contains("item3"));
568 }
569
570 #[test]
571 fn test_jsonparse_with_array() {
572 let mut engine = TemplateEngine::new();
573 engine.set_variable("json_string", r#"["apple", "banana", "cherry"]"#);
574
575 let template = r#"
576{% for fruit in json_string jsonparse %}
577- {{ fruit }}
578{% endfor %}"#;
579
580 let result = engine
581 .set_preserve_loop_newlines(false)
582 .render_string(template)
583 .unwrap();
584
585 let expected = r#"
586- apple
587- banana
588- cherry"#;
589 assert_eq!(result, expected);
590 }
591
592 #[test]
593 fn test_jsonparse_with_object() {
594 let mut engine = TemplateEngine::new();
595 engine.set_variable("json_object", r#"{"name": "Alice", "age": 30, "city": "Beijing"}"#);
596
597 let template = r#"
598{% for item in json_object jsonparse %}
599Key: {{ item.key }}, Value: {{ item.value }}
600{% endfor %}"#;
601
602 let result = engine
603 .set_preserve_loop_newlines(false)
604 .render_string(template)
605 .unwrap();
606
607 assert!(result.contains("Key: name, Value: Alice"));
608 assert!(result.contains("Key: age, Value: 30"));
609 assert!(result.contains("Key: city, Value: Beijing"));
610 }
611
612 #[test]
613 fn test_jsonparse_with_complex_array() {
614 let mut engine = TemplateEngine::new();
615 engine.set_variable("users_json", r#"[
616 {"name": "Alice", "age": 25},
617 {"name": "Bob", "age": 30},
618 {"name": "Charlie", "age": 35}
619 ]"#);
620
621 let template = r#"
622{% for user in users_json jsonparse %}
623- {{ user.name }} ({{ user.age }} years old)
624{% endfor %}"#;
625
626 let result = engine
627 .set_preserve_loop_newlines(false)
628 .render_string(template)
629 .unwrap();
630
631 assert!(result.contains("- Alice (25 years old)"));
632 assert!(result.contains("- Bob (30 years old)"));
633 assert!(result.contains("- Charlie (35 years old)"));
634 }
635
636 #[test]
637 fn test_jsonparse_invalid_json() {
638 let mut engine = TemplateEngine::new();
639 engine.set_variable("invalid_json", r#"["apple", "banana", "cherry""#);
640
641 let template = r#"
642{% for fruit in invalid_json jsonparse %}
643- {{ fruit }}
644{% endfor %}"#;
645
646 let result = engine.render_string(template);
647 assert!(result.is_err());
648 assert!(result.unwrap_err().to_string().contains("Failed to parse JSON"));
649 }
650
651 #[test]
652 fn test_jsonparse_non_string_variable() {
653 let mut engine = TemplateEngine::new();
654 engine.set_variable("already_array", json!(["apple", "banana", "cherry"]));
655
656 let template = r#"
657{% for fruit in already_array jsonparse %}
658- {{ fruit }}
659{% endfor %}"#;
660
661 let result = engine.render_string(template);
662 assert!(result.is_err());
663 assert!(result.unwrap_err().to_string().contains("Cannot jsonparse non-string variable"));
664 }
665}