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 let (result_after_sub, missing_vars) = Self::substitute_variables(&result, variables);
498
499 if let Some(first_missing) = missing_vars.first() {
501 return Err(TemplateError::MissingVariable(first_missing.clone()));
502 }
503
504 Ok(result_after_sub)
505 }
506
507 fn extract_partials(content: &str) -> Vec<(String, String)> {
511 let mut partials = Vec::new();
512 let bytes = content.as_bytes();
513
514 let mut i = 0;
515 while i < bytes.len().saturating_sub(2) {
516 if bytes[i] == b'{' && bytes[i + 1] == b'{' && i + 2 < bytes.len() {
518 let start = i;
519 i += 2;
520
521 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
523 i += 1;
524 }
525
526 if i < bytes.len() && bytes[i] == b'>' {
528 i += 1;
529
530 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
532 i += 1;
533 }
534
535 let name_start = i;
537 while i < bytes.len()
538 && !(bytes[i] == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}')
539 {
540 i += 1;
541 }
542
543 if i < bytes.len()
544 && bytes[i] == b'}'
545 && i + 1 < bytes.len()
546 && bytes[i + 1] == b'}'
547 {
548 let end = i + 2;
549 let full_match = &content[start..end];
550 let name = &content[name_start..i];
551
552 let partial_name = name.trim().to_string();
553 if !partial_name.is_empty() {
554 partials.push((full_match.to_string(), partial_name));
555 }
556 i = end;
557 continue;
558 }
559 }
560 }
561 i += 1;
562 }
563
564 partials
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn test_render_template() {
574 let template = Template::new("Hello {{NAME}}, your score is {{SCORE}}.");
575 let variables = HashMap::from([("NAME", "Alice".to_string()), ("SCORE", "42".to_string())]);
576 let rendered = template.render(&variables).unwrap();
577 assert_eq!(rendered, "Hello Alice, your score is 42.");
578 }
579
580 #[test]
581 fn test_missing_variable() {
582 let template = Template::new("Hello {{NAME}}.");
583 let variables = HashMap::new();
584 let result = template.render(&variables);
585 assert_eq!(
586 result,
587 Err(TemplateError::MissingVariable("NAME".to_string()))
588 );
589 }
590
591 #[test]
592 fn test_no_variables() {
593 let template = Template::new("Just plain text.");
594 let rendered = template.render(&HashMap::new()).unwrap();
595 assert_eq!(rendered, "Just plain text.");
596 }
597
598 #[test]
599 fn test_multiline_template() {
600 let template = Template::new("Review this:\n{{DIFF}}\nEnd of review.");
601 let variables = HashMap::from([("DIFF", "+ new line".to_string())]);
602 let rendered = template.render(&variables).unwrap();
603 assert_eq!(rendered, "Review this:\n+ new line\nEnd of review.");
604 }
605
606 #[test]
607 fn test_whitespace_in_variables() {
608 let template = Template::new("Value: {{ VALUE }}.");
609 let variables = HashMap::from([("VALUE", "42".to_string())]);
610 let rendered = template.render(&variables).unwrap();
611 assert_eq!(rendered, "Value: 42.");
612 }
613
614 #[test]
615 fn test_unclosed_opening_braces() {
616 let template = Template::new("Hello {{NAME and some text");
618 let rendered = template.render(&HashMap::new()).unwrap();
619 assert_eq!(rendered, "Hello {{NAME and some text");
621 }
622
623 #[test]
624 fn test_empty_variable_name() {
625 let template = Template::new("Value: {{}}.");
627 let rendered = template.render(&HashMap::new()).unwrap();
628 assert_eq!(rendered, "Value: {{}}.");
630 }
631
632 #[test]
633 fn test_whitespace_only_variable_name() {
634 let template = Template::new("Value: {{ }}.");
636 let rendered = template.render(&HashMap::new()).unwrap();
637 assert_eq!(rendered, "Value: {{ }}.");
639 }
640
641 #[test]
642 fn test_multiple_unclosed_braces() {
643 let template = Template::new("{{A text {{B text");
645 let rendered = template.render(&HashMap::new()).unwrap();
646 assert_eq!(rendered, "{{A text {{B text");
647 }
648
649 #[test]
650 fn test_partial_closing_brace() {
651 let template = Template::new("Hello {{NAME}} and {{VAR}} text");
653 let variables = HashMap::from([("NAME", "Alice".to_string()), ("VAR", "Bob".to_string())]);
654 let rendered = template.render(&variables).unwrap();
655 assert_eq!(rendered, "Hello Alice and Bob text");
656 }
657
658 #[test]
663 fn test_inline_comment_stripped() {
664 let template = Template::new("Hello {# this is a comment #}world.");
665 let rendered = template.render(&HashMap::new()).unwrap();
666 assert_eq!(rendered, "Hello world.");
667 }
668
669 #[test]
670 fn test_comment_on_own_line_stripped() {
671 let template = Template::new("Line 1\n{# This is a comment #}\nLine 2");
673 let rendered = template.render(&HashMap::new()).unwrap();
674 assert_eq!(rendered, "Line 1\nLine 2");
675 }
676
677 #[test]
678 fn test_multiline_comment() {
679 let template = Template::new("Before{# comment\nspanning\nlines #}After");
681 let rendered = template.render(&HashMap::new()).unwrap();
682 assert_eq!(rendered, "BeforeAfter");
683 }
684
685 #[test]
686 fn test_comment_at_end_of_content_line() {
687 let template = Template::new("Content{# comment #}\nMore");
689 let rendered = template.render(&HashMap::new()).unwrap();
690 assert_eq!(rendered, "Content\nMore");
691 }
692
693 #[test]
694 fn test_multiple_comments() {
695 let template = Template::new("{# first #}A{# second #}B{# third #}");
696 let rendered = template.render(&HashMap::new()).unwrap();
697 assert_eq!(rendered, "AB");
698 }
699
700 #[test]
701 fn test_comment_with_variable() {
702 let template = Template::new("{# doc comment #}\nHello {{NAME}}!");
704 let variables = HashMap::from([("NAME", "World".to_string())]);
705 let rendered = template.render(&variables).unwrap();
706 assert_eq!(rendered, "Hello World!");
707 }
708
709 #[test]
710 fn test_unclosed_comment_preserved() {
711 let template = Template::new("Hello {# unclosed comment");
713 let rendered = template.render(&HashMap::new()).unwrap();
714 assert_eq!(rendered, "Hello {# unclosed comment");
715 }
716
717 #[test]
718 fn test_comment_documentation_use_case() {
719 let content = r"{# Template Version: 1.0 #}
721{# This template generates commit messages #}
722You are a commit message expert.
723
724{# DIFF variable contains the git diff #}
725DIFF:
726{{DIFF}}
727
728{# End of template #}
729";
730 let template = Template::new(content);
731 let variables = HashMap::from([("DIFF", "+added line".to_string())]);
732 let rendered = template.render(&variables).unwrap();
733
734 assert!(!rendered.contains("Template Version"));
736 assert!(!rendered.contains("This template generates"));
737 assert!(!rendered.contains("DIFF variable contains"));
738 assert!(!rendered.contains("End of template"));
739
740 assert!(rendered.contains("You are a commit message expert."));
742 assert!(rendered.contains("+added line"));
743 }
744
745 #[test]
750 fn test_simple_partial_include() {
751 let partials = HashMap::from([("header".to_string(), "Common Header".to_string())]);
752 let template = Template::new("{{>header}}\nContent here");
753 let variables = HashMap::new();
754 let rendered = template
755 .render_with_partials(&variables, &partials)
756 .unwrap();
757 assert_eq!(rendered, "Common Header\nContent here");
758 }
759
760 #[test]
761 fn test_partial_with_whitespace() {
762 let partials = HashMap::from([("header".to_string(), "Header".to_string())]);
763 let template = Template::new("{{> header}}\nContent");
764 let variables = HashMap::new();
765 let rendered = template
766 .render_with_partials(&variables, &partials)
767 .unwrap();
768 assert_eq!(rendered, "Header\nContent");
769 }
770
771 #[test]
772 fn test_partial_with_variables() {
773 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}\n".to_string())]);
774 let template = Template::new("{{>greeting}}Body content");
775 let variables = HashMap::from([("NAME", "World".to_string())]);
776 let rendered = template
777 .render_with_partials(&variables, &partials)
778 .unwrap();
779 assert_eq!(rendered, "Hello World\nBody content");
780 }
781
782 #[test]
783 fn test_multiple_partials() {
784 let partials = HashMap::from([
785 ("header".to_string(), "=== HEADER ===\n".to_string()),
786 ("footer".to_string(), "\n=== FOOTER ===".to_string()),
787 ]);
788 let template = Template::new("{{>header}}Content{{>footer}}");
789 let variables = HashMap::new();
790 let rendered = template
791 .render_with_partials(&variables, &partials)
792 .unwrap();
793 assert_eq!(rendered, "=== HEADER ===\nContent\n=== FOOTER ===");
794 }
795
796 #[test]
797 fn test_nested_partials() {
798 let partials = HashMap::from([
799 (
800 "outer".to_string(),
801 "Outer start\n{{>inner}}\nOuter end".to_string(),
802 ),
803 ("inner".to_string(), "INNER CONTENT".to_string()),
804 ]);
805 let template = Template::new("{{>outer}}");
806 let variables = HashMap::new();
807 let rendered = template
808 .render_with_partials(&variables, &partials)
809 .unwrap();
810 assert_eq!(rendered, "Outer start\nINNER CONTENT\nOuter end");
811 }
812
813 #[test]
814 fn test_partial_not_found() {
815 let partials = HashMap::new();
816 let template = Template::new("{{>missing_partial}}");
817 let variables = HashMap::new();
818 let result = template.render_with_partials(&variables, &partials);
819 assert_eq!(
820 result,
821 Err(TemplateError::PartialNotFound(
822 "missing_partial".to_string()
823 ))
824 );
825 }
826
827 #[test]
828 fn test_circular_reference_detection() {
829 let partials = HashMap::from([
830 ("a".to_string(), "{{>b}}".to_string()),
831 ("b".to_string(), "{{>a}}".to_string()),
832 ]);
833 let template = Template::new("{{>a}}");
834 let variables = HashMap::new();
835 let result = template.render_with_partials(&variables, &partials);
836 match result {
837 Err(TemplateError::CircularReference(chain)) => {
838 assert_eq!(chain.len(), 3);
840 assert!(chain.contains(&"a".to_string()));
841 assert!(chain.contains(&"b".to_string()));
842 assert_eq!(chain.first(), chain.last());
844 }
845 _ => panic!("Expected CircularReference error"),
846 }
847 }
848
849 #[test]
850 fn test_self_referential_partial() {
851 let partials = HashMap::from([("loop".to_string(), "{{>loop}}".to_string())]);
852 let template = Template::new("{{>loop}}");
853 let variables = HashMap::new();
854 let result = template.render_with_partials(&variables, &partials);
855 match result {
856 Err(TemplateError::CircularReference(chain)) => {
857 assert_eq!(chain, vec!["loop".to_string(), "loop".to_string()]);
858 }
859 _ => panic!("Expected CircularReference error"),
860 }
861 }
862
863 #[test]
864 fn test_partial_with_missing_variable() {
865 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}".to_string())]);
866 let template = Template::new("{{>greeting}}");
867 let variables = HashMap::new(); let result = template.render_with_partials(&variables, &partials);
869 assert_eq!(
870 result,
871 Err(TemplateError::MissingVariable("NAME".to_string()))
872 );
873 }
874
875 #[test]
876 fn test_partial_and_main_variables() {
877 let partials = HashMap::from([("greeting".to_string(), "Hello {{NAME}}\n".to_string())]);
878 let template = Template::new("{{>greeting}}Your score is {{SCORE}}");
879 let variables = HashMap::from([("NAME", "Alice".to_string()), ("SCORE", "42".to_string())]);
880 let rendered = template
881 .render_with_partials(&variables, &partials)
882 .unwrap();
883 assert_eq!(rendered, "Hello Alice\nYour score is 42");
884 }
885
886 #[test]
887 fn test_partial_with_comments() {
888 let partials = HashMap::from([(
889 "header".to_string(),
890 "{# This is a header #}Header Content\n".to_string(),
891 )]);
892 let template = Template::new("{{>header}}Body");
893 let variables = HashMap::new();
894 let rendered = template
895 .render_with_partials(&variables, &partials)
896 .unwrap();
897 assert_eq!(rendered, "Header Content\nBody");
898 }
899
900 #[test]
901 fn test_partial_with_path_style_name() {
902 let partials = HashMap::from([("shared/_header".to_string(), "Shared Header".to_string())]);
903 let template = Template::new("{{> shared/_header}}\nContent");
904 let variables = HashMap::new();
905 let rendered = template
906 .render_with_partials(&variables, &partials)
907 .unwrap();
908 assert_eq!(rendered, "Shared Header\nContent");
909 }
910
911 #[test]
912 fn test_backward_compatibility_render_without_partials() {
913 let template = Template::new("Hello {{NAME}}");
915 let variables = HashMap::from([("NAME", "World".to_string())]);
916 let rendered = template.render(&variables).unwrap();
917 assert_eq!(rendered, "Hello World");
918 }
919
920 #[test]
921 fn test_empty_partial_name_ignored() {
922 let template = Template::new("Before {{> }} After");
924 let variables = HashMap::new();
925 let rendered = template.render(&variables).unwrap();
926 assert_eq!(rendered, "Before {{> }} After");
927 }
928
929 #[test]
934 fn test_conditional_with_true_variable() {
935 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
936 let variables = HashMap::from([("NAME", "World".to_string())]);
937 let rendered = template.render(&variables).unwrap();
938 assert_eq!(rendered, "Hello World");
939 }
940
941 #[test]
942 fn test_conditional_with_false_variable() {
943 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
944 let variables = HashMap::new(); let rendered = template.render(&variables).unwrap();
946 assert_eq!(rendered, "");
947 }
948
949 #[test]
950 fn test_conditional_with_empty_variable() {
951 let template = Template::new("{% if NAME %}Hello {{NAME}}{% endif %}");
952 let variables = HashMap::from([("NAME", String::new())]);
953 let rendered = template.render(&variables).unwrap();
954 assert_eq!(rendered, "");
955 }
956
957 #[test]
958 fn test_conditional_with_negation_true() {
959 let template = Template::new("{% if !NAME %}No name{% endif %}");
960 let variables = HashMap::new(); let rendered = template.render(&variables).unwrap();
962 assert_eq!(rendered, "No name");
963 }
964
965 #[test]
966 fn test_conditional_with_negation_false() {
967 let template = Template::new("{% if !NAME %}No name{% endif %}");
968 let variables = HashMap::from([("NAME", "Alice".to_string())]);
969 let rendered = template.render(&variables).unwrap();
970 assert_eq!(rendered, "");
971 }
972
973 #[test]
974 fn test_multiple_conditionals() {
975 let template = Template::new(
976 "{% if GREETING %}{{GREETING}}{% endif %} {% if NAME %}{{NAME}}{% endif %}",
977 );
978 let variables = HashMap::from([("NAME", "Bob".to_string())]);
979 let rendered = template.render(&variables).unwrap();
980 assert_eq!(rendered, " Bob");
981 }
982
983 #[test]
984 fn test_conditional_with_surrounding_content() {
985 let template = Template::new("Start {% if SHOW %}shown{% endif %} End");
986 let variables = HashMap::from([("SHOW", "yes".to_string())]);
987 let rendered = template.render(&variables).unwrap();
988 assert_eq!(rendered, "Start shown End");
989 }
990
991 #[test]
996 fn test_default_value_with_missing_variable() {
997 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
998 let variables = HashMap::new();
999 let rendered = template.render(&variables).unwrap();
1000 assert_eq!(rendered, "Hello Guest");
1001 }
1002
1003 #[test]
1004 fn test_default_value_with_empty_variable() {
1005 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
1006 let variables = HashMap::from([("NAME", String::new())]);
1007 let rendered = template.render(&variables).unwrap();
1008 assert_eq!(rendered, "Hello Guest");
1009 }
1010
1011 #[test]
1012 fn test_default_value_with_present_variable() {
1013 let template = Template::new("Hello {{NAME|default=\"Guest\"}}");
1014 let variables = HashMap::from([("NAME", "Alice".to_string())]);
1015 let rendered = template.render(&variables).unwrap();
1016 assert_eq!(rendered, "Hello Alice");
1017 }
1018
1019 #[test]
1020 fn test_default_value_with_single_quotes() {
1021 let template = Template::new("Hello {{NAME|default='Guest'}}");
1022 let variables = HashMap::new();
1023 let rendered = template.render(&variables).unwrap();
1024 assert_eq!(rendered, "Hello Guest");
1025 }
1026
1027 #[test]
1032 fn test_loop_with_items() {
1033 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1034 let variables = HashMap::from([("ITEMS", "apple,banana,cherry".to_string())]);
1035 let rendered = template.render(&variables).unwrap();
1036 assert_eq!(rendered, "apple banana cherry ");
1037 }
1038
1039 #[test]
1040 fn test_loop_with_empty_list() {
1041 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1042 let variables = HashMap::from([("ITEMS", String::new())]);
1043 let rendered = template.render(&variables).unwrap();
1044 assert_eq!(rendered, "");
1045 }
1046
1047 #[test]
1048 fn test_loop_with_missing_variable() {
1049 let template = Template::new("{% for item in ITEMS %}{{item}} {% endfor %}");
1050 let variables = HashMap::new();
1051 let rendered = template.render(&variables).unwrap();
1052 assert_eq!(rendered, "");
1053 }
1054
1055 #[test]
1056 fn test_loop_with_conditional_inside() {
1057 let template =
1058 Template::new("{% for item in ITEMS %}{% if item %}{{item}} {% endif %}{% endfor %}");
1059 let variables = HashMap::from([("ITEMS", "apple,,cherry".to_string())]);
1060 let rendered = template.render(&variables).unwrap();
1061 assert_eq!(rendered, "apple cherry ");
1062 }
1063}