1use std::collections::HashMap;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum TemplateError {
37 MissingVariable(String),
39 PartialNotFound(String),
41 CircularReference(Vec<String>),
43}
44
45impl std::fmt::Display for TemplateError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::MissingVariable(name) => write!(f, "Missing required variable: {{{{ {name} }}}}"),
49 Self::PartialNotFound(name) => {
50 write!(f, "Partial not found: '{{> {name}}}'")
51 }
52 Self::CircularReference(chain) => {
53 write!(f, "Circular reference detected in partials: ")?;
54 let mut sep = "";
55 for partial in chain {
56 write!(f, "{sep}{{{{> {partial}}}}}")?;
57 sep = " -> ";
58 }
59 Ok(())
60 }
61 }
62 }
63}
64
65impl std::error::Error for TemplateError {}
66
67#[derive(Debug, Clone)]
82pub struct Template {
83 content: String,
85}
86
87impl Template {
88 pub fn new(content: &str) -> Self {
93 let content = Self::strip_comments(content);
95 Self { content }
96 }
97
98 fn strip_comments(content: &str) -> String {
103 let mut result = String::with_capacity(content.len());
104 let bytes = content.as_bytes();
105
106 let mut i = 0;
107 while i < bytes.len() {
108 if i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'#' {
110 let comment_start = i;
112 i += 2;
113 while i + 1 < bytes.len() && !(bytes[i] == b'#' && bytes[i + 1] == b'}') {
114 i += 1;
115 }
116 if i + 1 < bytes.len() && bytes[i] == b'#' && bytes[i + 1] == b'}' {
117 i += 2;
118 let whitespace_start = i;
121 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
122 i += 1;
123 }
124 if i < bytes.len() && bytes[i] == b'\n' {
126 let was_line_start = result.is_empty() || result.ends_with('\n');
128 if was_line_start {
129 i += 1;
131 } else {
132 i = whitespace_start;
134 }
135 } else if i < bytes.len() {
136 i = whitespace_start;
138 }
139 continue;
140 }
141 result.push_str(&content[comment_start..i]);
143 } else {
144 result.push(bytes[i] as char);
145 i += 1;
146 }
147 }
148
149 result
150 }
151
152 fn process_conditionals(content: &str, variables: &HashMap<&str, String>) -> String {
160 let mut result = content.to_string();
161
162 while let Some(start) = result.find("{% if ") {
164 let if_end_start = start + 6; let if_end = if let Some(pos) = result[if_end_start..].find("%}") {
167 if_end_start + pos + 2
168 } else {
169 result = result[start + 1..].to_string();
171 continue;
172 };
173
174 let condition = result[if_end_start..if_end - 2].trim().to_string();
176
177 let endif_start = if let Some(pos) = result[if_end..].find("{% endif %}") {
179 if_end + pos
180 } else {
181 result = result[start + 1..].to_string();
183 continue;
184 };
185
186 let endif_end = endif_start + 11; let block_content = result[if_end..endif_start].to_string();
190
191 let should_show = Self::evaluate_condition(&condition, variables);
193
194 let replacement = if should_show {
196 block_content
197 } else {
198 String::new()
199 };
200 result.replace_range(start..endif_end, &replacement);
201 }
202
203 result
204 }
205
206 fn evaluate_condition(condition: &str, variables: &HashMap<&str, String>) -> bool {
212 let condition = condition.trim();
213
214 if let Some(rest) = condition.strip_prefix('!') {
216 let var_name = rest.trim();
217 let value = variables.get(var_name);
218 return value.is_none_or(String::is_empty);
219 }
220
221 let value = variables.get(condition);
223 value.is_some_and(|v| !v.is_empty())
224 }
225
226 fn process_loops(content: &str, variables: &HashMap<&str, String>) -> String {
233 let mut result = content.to_string();
234
235 while let Some(start) = result.find("{% for ") {
237 let for_end_start = start + 7; let for_end = if let Some(pos) = result[for_end_start..].find("%}") {
240 for_end_start + pos + 2
241 } else {
242 result = result[start + 1..].to_string();
244 continue;
245 };
246
247 let condition = result[for_end_start..for_end - 2].trim();
249 let parts: Vec<&str> = condition.split(" in ").collect();
250 if parts.len() != 2 {
251 result = result[start + 1..].to_string();
253 continue;
254 }
255
256 let loop_var = parts[0].trim().to_string();
257 let list_var = parts[1].trim();
258
259 let endfor_start = if let Some(pos) = result[for_end..].find("{% endfor %}") {
261 for_end + pos
262 } else {
263 result = result[start + 1..].to_string();
265 continue;
266 };
267
268 let endfor_end = endfor_start + 12; let block_template = result[for_end..endfor_start].to_string();
272
273 let items: Vec<String> = variables.get(list_var).map_or(Vec::new(), |v| {
275 if v.is_empty() {
276 Vec::new()
277 } else {
278 v.split(',').map(|s| s.trim().to_string()).collect()
280 }
281 });
282
283 let mut loop_output = String::new();
285 for item in items {
286 let mut loop_vars: HashMap<&str, String> = variables.clone();
288 loop_vars.insert(&loop_var, item);
289
290 let processed = Self::process_conditionals(&block_template, &loop_vars);
292
293 let (processed, _missing) = Self::substitute_variables(&processed, &loop_vars);
295 loop_output.push_str(&processed);
296 }
297
298 result.replace_range(start..endfor_end, &loop_output);
300 }
301
302 result
303 }
304
305 fn substitute_variables(
309 content: &str,
310 variables: &HashMap<&str, String>,
311 ) -> (String, Vec<String>) {
312 let mut result = content.to_string();
313 let mut missing_vars = Vec::new();
314
315 let mut replacements = Vec::new();
317 let mut i = 0;
318 let bytes = content.as_bytes();
319 while i < bytes.len().saturating_sub(1) {
320 if bytes[i] == b'{' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
321 let start = i;
322 i += 2;
323
324 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
326 i += 1;
327 }
328
329 let name_start = i;
330
331 while i < bytes.len()
333 && !(bytes[i] == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}')
334 {
335 i += 1;
336 }
337
338 if i < bytes.len()
339 && bytes[i] == b'}'
340 && i + 1 < bytes.len()
341 && bytes[i + 1] == b'}'
342 {
343 let end = i + 2;
344 let var_spec = &content[name_start..i];
345
346 if var_spec.trim().starts_with('>') {
348 i = end;
349 continue;
350 }
351
352 let trimmed_var = var_spec.trim();
354 if trimmed_var.is_empty() {
355 i = end;
356 continue;
357 }
358
359 let (var_name, default_value) =
361 var_spec.find('|').map_or((trimmed_var, None), |pipe_pos| {
362 let name = var_spec[..pipe_pos].trim();
363 let rest = &var_spec[pipe_pos + 1..];
364 rest.find('=').map_or((name, None), |eq_pos| {
366 let key = rest[..eq_pos].trim();
367 if key == "default" {
368 let value = rest[eq_pos + 1..].trim();
369 let value = if (value.starts_with('"') && value.ends_with('"'))
371 || (value.starts_with('\'') && value.ends_with('\''))
372 {
373 &value[1..value.len() - 1]
374 } else {
375 value
376 };
377 (name, Some(value.to_string()))
378 } else {
379 (name, None)
380 }
381 })
382 });
383
384 let (replacement, should_replace) = variables.get(var_name).map_or_else(
386 || {
387 default_value.as_ref().map_or_else(
388 || {
389 missing_vars.push(var_name.to_string());
391 (String::new(), false)
392 },
393 |default| (default.clone(), true),
394 )
395 },
396 |value| {
397 if !value.is_empty() {
398 (value.clone(), true)
399 } else if let Some(default) = &default_value {
400 (default.clone(), true)
401 } else {
402 (String::new(), false)
404 }
405 },
406 );
407
408 if should_replace {
409 replacements.push((start, end, replacement));
410 }
411 i = end;
412 continue;
413 }
414 }
415 i += 1;
416 }
417
418 for (start, end, replacement) in replacements.into_iter().rev() {
420 result.replace_range(start..end, &replacement);
421 }
422
423 (result, missing_vars)
424 }
425
426 pub fn render(&self, variables: &HashMap<&str, String>) -> Result<String, TemplateError> {
428 let mut result = Self::process_loops(&self.content, variables);
430
431 result = Self::process_conditionals(&result, variables);
433
434 let (result_after_sub, missing_vars) = Self::substitute_variables(&result, variables);
436
437 if let Some(first_missing) = missing_vars.first() {
439 return Err(TemplateError::MissingVariable(first_missing.clone()));
440 }
441
442 Ok(result_after_sub)
443 }
444
445 pub fn render_with_partials(
450 &self,
451 variables: &HashMap<&str, String>,
452 partials: &HashMap<String, String>,
453 ) -> Result<String, TemplateError> {
454 self.render_with_partials_recursive(variables, partials, &mut Vec::new())
455 }
456
457 fn render_with_partials_recursive(
460 &self,
461 variables: &HashMap<&str, String>,
462 partials: &HashMap<String, String>,
463 visited: &mut Vec<String>,
464 ) -> Result<String, TemplateError> {
465 let mut result = self.content.clone();
467
468 let partial_refs = Self::extract_partials(&result);
470
471 for (full_match, partial_name) in partial_refs.into_iter().rev() {
473 if visited.contains(&partial_name) {
475 let mut chain = visited.clone();
476 chain.push(partial_name);
477 return Err(TemplateError::CircularReference(chain));
478 }
479
480 let partial_content = partials
482 .get(&partial_name)
483 .ok_or_else(|| TemplateError::PartialNotFound(partial_name.clone()))?;
484
485 let partial_template = Self::new(partial_content);
487 visited.push(partial_name.clone());
488 let rendered_partial =
489 partial_template.render_with_partials_recursive(variables, partials, visited)?;
490 visited.pop();
491
492 result = result.replace(&full_match, &rendered_partial);
494 }
495
496 result = Self::process_loops(&result, variables);
498
499 result = Self::process_conditionals(&result, variables);
501
502 let (result_after_sub, missing_vars) = Self::substitute_variables(&result, variables);
504
505 if let Some(first_missing) = missing_vars.first() {
507 return Err(TemplateError::MissingVariable(first_missing.clone()));
508 }
509
510 Ok(result_after_sub)
511 }
512
513 fn extract_partials(content: &str) -> Vec<(String, String)> {
517 let mut partials = Vec::new();
518 let bytes = content.as_bytes();
519
520 let mut i = 0;
521 while i < bytes.len().saturating_sub(2) {
522 if bytes[i] == b'{' && bytes[i + 1] == b'{' && i + 2 < bytes.len() {
524 let start = i;
525 i += 2;
526
527 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
529 i += 1;
530 }
531
532 if i < bytes.len() && bytes[i] == b'>' {
534 i += 1;
535
536 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
538 i += 1;
539 }
540
541 let name_start = i;
543 while i < bytes.len()
544 && !(bytes[i] == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}')
545 {
546 i += 1;
547 }
548
549 if i < bytes.len()
550 && bytes[i] == b'}'
551 && i + 1 < bytes.len()
552 && bytes[i + 1] == b'}'
553 {
554 let end = i + 2;
555 let full_match = &content[start..end];
556 let name = &content[name_start..i];
557
558 let partial_name = name.trim().to_string();
559 if !partial_name.is_empty() {
560 partials.push((full_match.to_string(), partial_name));
561 }
562 i = end;
563 continue;
564 }
565 }
566 }
567 i += 1;
568 }
569
570 partials
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[test]
579 fn test_render_template() {
580 let template = Template::new("Hello {{NAME}}, your score is {{SCORE}}.");
581 let variables = HashMap::from([("NAME", "Alice".to_string()), ("SCORE", "42".to_string())]);
582 let rendered = template.render(&variables).unwrap();
583 assert_eq!(rendered, "Hello Alice, your score is 42.");
584 }
585
586 #[test]
587 fn test_missing_variable() {
588 let template = Template::new("Hello {{NAME}}.");
589 let variables = HashMap::new();
590 let result = template.render(&variables);
591 assert_eq!(
592 result,
593 Err(TemplateError::MissingVariable("NAME".to_string()))
594 );
595 }
596
597 #[test]
598 fn test_no_variables() {
599 let template = Template::new("Just plain text.");
600 let rendered = template.render(&HashMap::new()).unwrap();
601 assert_eq!(rendered, "Just plain text.");
602 }
603
604 #[test]
605 fn test_multiline_template() {
606 let template = Template::new("Review this:\n{{DIFF}}\nEnd of review.");
607 let variables = HashMap::from([("DIFF", "+ new line".to_string())]);
608 let rendered = template.render(&variables).unwrap();
609 assert_eq!(rendered, "Review this:\n+ new line\nEnd of review.");
610 }
611
612 #[test]
613 fn test_whitespace_in_variables() {
614 let template = Template::new("Value: {{ VALUE }}.");
615 let variables = HashMap::from([("VALUE", "42".to_string())]);
616 let rendered = template.render(&variables).unwrap();
617 assert_eq!(rendered, "Value: 42.");
618 }
619
620 #[test]
621 fn test_unclosed_opening_braces() {
622 let template = Template::new("Hello {{NAME and some text");
624 let rendered = template.render(&HashMap::new()).unwrap();
625 assert_eq!(rendered, "Hello {{NAME and some text");
627 }
628
629 #[test]
630 fn test_empty_variable_name() {
631 let template = Template::new("Value: {{}}.");
633 let rendered = template.render(&HashMap::new()).unwrap();
634 assert_eq!(rendered, "Value: {{}}.");
636 }
637
638 #[test]
639 fn test_whitespace_only_variable_name() {
640 let template = Template::new("Value: {{ }}.");
642 let rendered = template.render(&HashMap::new()).unwrap();
643 assert_eq!(rendered, "Value: {{ }}.");
645 }
646
647 #[test]
648 fn test_multiple_unclosed_braces() {
649 let template = Template::new("{{A text {{B text");
651 let rendered = template.render(&HashMap::new()).unwrap();
652 assert_eq!(rendered, "{{A text {{B text");
653 }
654
655 #[test]
656 fn test_partial_closing_brace() {
657 let template = Template::new("Hello {{NAME}} and {{VAR}} text");
659 let variables = HashMap::from([("NAME", "Alice".to_string()), ("VAR", "Bob".to_string())]);
660 let rendered = template.render(&variables).unwrap();
661 assert_eq!(rendered, "Hello Alice and Bob text");
662 }
663
664 #[test]
669 fn test_inline_comment_stripped() {
670 let template = Template::new("Hello {# this is a comment #}world.");
671 let rendered = template.render(&HashMap::new()).unwrap();
672 assert_eq!(rendered, "Hello world.");
673 }
674
675 #[test]
676 fn test_comment_on_own_line_stripped() {
677 let template = Template::new("Line 1\n{# This is a comment #}\nLine 2");
679 let rendered = template.render(&HashMap::new()).unwrap();
680 assert_eq!(rendered, "Line 1\nLine 2");
681 }
682
683 #[test]
684 fn test_multiline_comment() {
685 let template = Template::new("Before{# comment\nspanning\nlines #}After");
687 let rendered = template.render(&HashMap::new()).unwrap();
688 assert_eq!(rendered, "BeforeAfter");
689 }
690
691 #[test]
692 fn test_comment_at_end_of_content_line() {
693 let template = Template::new("Content{# comment #}\nMore");
695 let rendered = template.render(&HashMap::new()).unwrap();
696 assert_eq!(rendered, "Content\nMore");
697 }
698
699 #[test]
700 fn test_multiple_comments() {
701 let template = Template::new("{# first #}A{# second #}B{# third #}");
702 let rendered = template.render(&HashMap::new()).unwrap();
703 assert_eq!(rendered, "AB");
704 }
705
706 #[test]
707 fn test_comment_with_variable() {
708 let template = Template::new("{# doc comment #}\nHello {{NAME}}!");
710 let variables = HashMap::from([("NAME", "World".to_string())]);
711 let rendered = template.render(&variables).unwrap();
712 assert_eq!(rendered, "Hello World!");
713 }
714
715 #[test]
716 fn test_unclosed_comment_preserved() {
717 let template = Template::new("Hello {# unclosed comment");
719 let rendered = template.render(&HashMap::new()).unwrap();
720 assert_eq!(rendered, "Hello {# unclosed comment");
721 }
722
723 #[test]
724 fn test_comment_documentation_use_case() {
725 let content = r"{# Template Version: 1.0 #}
727{# This template generates commit messages #}
728You are a commit message expert.
729
730{# DIFF variable contains the git diff #}
731DIFF:
732{{DIFF}}
733
734{# End of template #}
735";
736 let template = Template::new(content);
737 let variables = HashMap::from([("DIFF", "+added line".to_string())]);
738 let rendered = template.render(&variables).unwrap();
739
740 assert!(!rendered.contains("Template Version"));
742 assert!(!rendered.contains("This template generates"));
743 assert!(!rendered.contains("DIFF variable contains"));
744 assert!(!rendered.contains("End of template"));
745
746 assert!(rendered.contains("You are a commit message expert."));
748 assert!(rendered.contains("+added line"));
749 }
750
751 #[test]
756 fn test_simple_partial_include() {
757 let partials = HashMap::from([("header".to_string(), "Common Header".to_string())]);
758 let template = Template::new("{{>header}}\nContent here");
759 let variables = HashMap::new();
760 let rendered = template
761 .render_with_partials(&variables, &partials)
762 .unwrap();
763 assert_eq!(rendered, "Common Header\nContent here");
764 }
765
766 #[test]
767 fn test_partial_with_whitespace() {
768 let partials = HashMap::from([("header".to_string(), "Header".to_string())]);
769 let template = Template::new("{{> header}}\nContent");
770 let variables = HashMap::new();
771 let rendered = template
772 .render_with_partials(&variables, &partials)
773 .unwrap();
774 assert_eq!(rendered, "Header\nContent");
775 }
776
777 #[test]
778 fn test_partial_with_variables() {
779 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}\n".to_string())]);
780 let template = Template::new("{{>greeting}}Body content");
781 let variables = HashMap::from([("NAME", "World".to_string())]);
782 let rendered = template
783 .render_with_partials(&variables, &partials)
784 .unwrap();
785 assert_eq!(rendered, "Hello World\nBody content");
786 }
787
788 #[test]
789 fn test_multiple_partials() {
790 let partials = HashMap::from([
791 ("header".to_string(), "=== HEADER ===\n".to_string()),
792 ("footer".to_string(), "\n=== FOOTER ===".to_string()),
793 ]);
794 let template = Template::new("{{>header}}Content{{>footer}}");
795 let variables = HashMap::new();
796 let rendered = template
797 .render_with_partials(&variables, &partials)
798 .unwrap();
799 assert_eq!(rendered, "=== HEADER ===\nContent\n=== FOOTER ===");
800 }
801
802 #[test]
803 fn test_nested_partials() {
804 let partials = HashMap::from([
805 (
806 "outer".to_string(),
807 "Outer start\n{{>inner}}\nOuter end".to_string(),
808 ),
809 ("inner".to_string(), "INNER CONTENT".to_string()),
810 ]);
811 let template = Template::new("{{>outer}}");
812 let variables = HashMap::new();
813 let rendered = template
814 .render_with_partials(&variables, &partials)
815 .unwrap();
816 assert_eq!(rendered, "Outer start\nINNER CONTENT\nOuter end");
817 }
818
819 #[test]
820 fn test_partial_not_found() {
821 let partials = HashMap::new();
822 let template = Template::new("{{>missing_partial}}");
823 let variables = HashMap::new();
824 let result = template.render_with_partials(&variables, &partials);
825 assert_eq!(
826 result,
827 Err(TemplateError::PartialNotFound(
828 "missing_partial".to_string()
829 ))
830 );
831 }
832
833 #[test]
834 fn test_circular_reference_detection() {
835 let partials = HashMap::from([
836 ("a".to_string(), "{{>b}}".to_string()),
837 ("b".to_string(), "{{>a}}".to_string()),
838 ]);
839 let template = Template::new("{{>a}}");
840 let variables = HashMap::new();
841 let result = template.render_with_partials(&variables, &partials);
842 match result {
843 Err(TemplateError::CircularReference(chain)) => {
844 assert_eq!(chain.len(), 3);
846 assert!(chain.contains(&"a".to_string()));
847 assert!(chain.contains(&"b".to_string()));
848 assert_eq!(chain.first(), chain.last());
850 }
851 _ => panic!("Expected CircularReference error"),
852 }
853 }
854
855 #[test]
856 fn test_self_referential_partial() {
857 let partials = HashMap::from([("loop".to_string(), "{{>loop}}".to_string())]);
858 let template = Template::new("{{>loop}}");
859 let variables = HashMap::new();
860 let result = template.render_with_partials(&variables, &partials);
861 match result {
862 Err(TemplateError::CircularReference(chain)) => {
863 assert_eq!(chain, vec!["loop".to_string(), "loop".to_string()]);
864 }
865 _ => panic!("Expected CircularReference error"),
866 }
867 }
868
869 #[test]
870 fn test_partial_with_missing_variable() {
871 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}".to_string())]);
872 let template = Template::new("{{>greeting}}");
873 let variables = HashMap::new(); let result = template.render_with_partials(&variables, &partials);
875 assert_eq!(
876 result,
877 Err(TemplateError::MissingVariable("NAME".to_string()))
878 );
879 }
880
881 #[test]
882 fn test_partial_and_main_variables() {
883 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}\n".to_string())]);
884 let template = Template::new("{{>greeting}}Your score is {{SCORE}}");
885 let variables = HashMap::from([("NAME", "Alice".to_string()), ("SCORE", "42".to_string())]);
886 let rendered = template
887 .render_with_partials(&variables, &partials)
888 .unwrap();
889 assert_eq!(rendered, "Hello Alice\nYour score is 42");
890 }
891
892 #[test]
893 fn test_partial_with_comments() {
894 let partials = HashMap::from([(
895 "header".to_string(),
896 "{# This is a header #}Header Content\n".to_string(),
897 )]);
898 let template = Template::new("{{>header}}Body");
899 let variables = HashMap::new();
900 let rendered = template
901 .render_with_partials(&variables, &partials)
902 .unwrap();
903 assert_eq!(rendered, "Header Content\nBody");
904 }
905
906 #[test]
907 fn test_partial_with_path_style_name() {
908 let partials = HashMap::from([("shared/_header".to_string(), "Shared Header".to_string())]);
909 let template = Template::new("{{> shared/_header}}\nContent");
910 let variables = HashMap::new();
911 let rendered = template
912 .render_with_partials(&variables, &partials)
913 .unwrap();
914 assert_eq!(rendered, "Shared Header\nContent");
915 }
916
917 #[test]
918 fn test_backward_compatibility_render_without_partials() {
919 let template = Template::new("Hello {{NAME}}");
921 let variables = HashMap::from([("NAME", "World".to_string())]);
922 let rendered = template.render(&variables).unwrap();
923 assert_eq!(rendered, "Hello World");
924 }
925
926 #[test]
927 fn test_empty_partial_name_ignored() {
928 let template = Template::new("Before {{> }} After");
930 let variables = HashMap::new();
931 let rendered = template.render(&variables).unwrap();
932 assert_eq!(rendered, "Before {{> }} After");
933 }
934
935 #[test]
940 fn test_conditional_with_true_variable() {
941 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
942 let variables = HashMap::from([("NAME", "World".to_string())]);
943 let rendered = template.render(&variables).unwrap();
944 assert_eq!(rendered, "Hello World");
945 }
946
947 #[test]
948 fn test_conditional_with_false_variable() {
949 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
950 let variables = HashMap::new(); let rendered = template.render(&variables).unwrap();
952 assert_eq!(rendered, "");
953 }
954
955 #[test]
956 fn test_conditional_with_empty_variable() {
957 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
958 let variables = HashMap::from([("NAME", String::new())]);
959 let rendered = template.render(&variables).unwrap();
960 assert_eq!(rendered, "");
961 }
962
963 #[test]
964 fn test_conditional_with_negation_true() {
965 let template = Template::new("{% if !NAME %}No name{% endif %}");
966 let variables = HashMap::new(); let rendered = template.render(&variables).unwrap();
968 assert_eq!(rendered, "No name");
969 }
970
971 #[test]
972 fn test_conditional_with_negation_false() {
973 let template = Template::new("{% if !NAME %}No name{% endif %}");
974 let variables = HashMap::from([("NAME", "Alice".to_string())]);
975 let rendered = template.render(&variables).unwrap();
976 assert_eq!(rendered, "");
977 }
978
979 #[test]
980 fn test_multiple_conditionals() {
981 let template = Template::new(
982 "{% if GREETING %}{{GREETING}}{% endif %} {% if NAME %}{{NAME}}{% endif %}",
983 );
984 let variables = HashMap::from([("NAME", "Bob".to_string())]);
985 let rendered = template.render(&variables).unwrap();
986 assert_eq!(rendered, " Bob");
987 }
988
989 #[test]
990 fn test_conditional_with_surrounding_content() {
991 let template = Template::new("Start {% if SHOW %}shown{% endif %} End");
992 let variables = HashMap::from([("SHOW", "yes".to_string())]);
993 let rendered = template.render(&variables).unwrap();
994 assert_eq!(rendered, "Start shown End");
995 }
996
997 #[test]
1002 fn test_default_value_with_missing_variable() {
1003 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
1004 let variables = HashMap::new();
1005 let rendered = template.render(&variables).unwrap();
1006 assert_eq!(rendered, "Hello Guest");
1007 }
1008
1009 #[test]
1010 fn test_default_value_with_empty_variable() {
1011 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
1012 let variables = HashMap::from([("NAME", String::new())]);
1013 let rendered = template.render(&variables).unwrap();
1014 assert_eq!(rendered, "Hello Guest");
1015 }
1016
1017 #[test]
1018 fn test_default_value_with_present_variable() {
1019 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
1020 let variables = HashMap::from([("NAME", "Alice".to_string())]);
1021 let rendered = template.render(&variables).unwrap();
1022 assert_eq!(rendered, "Hello Alice");
1023 }
1024
1025 #[test]
1026 fn test_default_value_with_single_quotes() {
1027 let template = Template::new("Hello {{NAME|default='Guest'}}");
1028 let variables = HashMap::new();
1029 let rendered = template.render(&variables).unwrap();
1030 assert_eq!(rendered, "Hello Guest");
1031 }
1032
1033 #[test]
1038 fn test_loop_with_items() {
1039 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1040 let variables = HashMap::from([("ITEMS", "apple,banana,cherry".to_string())]);
1041 let rendered = template.render(&variables).unwrap();
1042 assert_eq!(rendered, "apple banana cherry ");
1043 }
1044
1045 #[test]
1046 fn test_loop_with_empty_list() {
1047 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1048 let variables = HashMap::from([("ITEMS", String::new())]);
1049 let rendered = template.render(&variables).unwrap();
1050 assert_eq!(rendered, "");
1051 }
1052
1053 #[test]
1054 fn test_loop_with_missing_variable() {
1055 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1056 let variables = HashMap::new();
1057 let rendered = template.render(&variables).unwrap();
1058 assert_eq!(rendered, "");
1059 }
1060
1061 #[test]
1062 fn test_loop_with_conditional_inside() {
1063 let template =
1064 Template::new("{% for item in ITEMS %}{% if item %}{{item}} {% endif %}{% endfor %}");
1065 let variables = HashMap::from([("ITEMS", "apple,,cherry".to_string())]);
1066 let rendered = template.render(&variables).unwrap();
1067 assert_eq!(rendered, "apple cherry ");
1068 }
1069}