1use alloc::{
2 collections::BTreeMap,
3 format,
4 string::{String, ToString},
5 vec,
6 vec::Vec,
7};
8
9use crate::{
10 ast::{Node, NodeList},
11 context::{Context, IntoContext},
12 error::Error,
13 parser::TemplateParser,
14 value::Value,
15 vm::Renderer,
16};
17
18const MAX_EXTEND_DEPTH: usize = 128;
19
20#[derive(Clone, Debug)]
25pub enum EscapeMode {
26 Html,
27 Text,
28}
29
30pub(crate) struct ParsedTemplate {
31 pub(crate) nodes: NodeList,
32}
33
34pub struct Engine {
39 pub(crate) mode: EscapeMode,
40 pub(crate) templates: BTreeMap<String, ParsedTemplate>,
41}
42
43impl Engine {
44 pub fn new(mode: EscapeMode) -> Self {
46 Self {
47 mode,
48 templates: BTreeMap::new(),
49 }
50 }
51
52 pub fn add_template(&mut self, name: &str, source: &str) -> Result<(), Error> {
58 if self.templates.contains_key(name) {
59 return Err(Error::parse(format!("template `{name}` already exists")));
60 }
61 let mut parser = TemplateParser::new(source);
62 let nodes = parser.parse()?;
63
64 self.templates.insert(
65 name.to_string(),
66 ParsedTemplate {
67 nodes,
68 },
69 );
70 Ok(())
71 }
72
73 pub fn render(&self, name: &str, context: impl IntoContext) -> Result<String, Error> {
92 let template = self
93 .templates
94 .get(name)
95 .ok_or_else(|| Error::undefined_template(name))?;
96
97 let context: Context = context.into_context().map_err(|e| Error::parse(e.0))?;
98
99 let mut output = String::new();
100
101 self.render_with_extends(&template.nodes, &context.0, &mut output, None, 0)?;
102
103 Ok(output)
104 }
105
106 fn render_with_extends(
107 &self,
108 nodes: &NodeList,
109 variables: &Value,
110 output: &mut String,
111 parent_blocks: Option<&BTreeMap<String, Vec<NodeList>>>,
112 extend_depth: usize,
113 ) -> Result<(), Error> {
114 if extend_depth >= MAX_EXTEND_DEPTH {
115 return Err(Error::render(format!("extend depth limit ({MAX_EXTEND_DEPTH}) exceeded")));
116 }
117 let extends_name = nodes.iter().find_map(|n| {
119 if let Node::Extends(name) = n {
120 Some(name.clone())
121 } else {
122 None
123 }
124 });
125
126 if let Some(parent_name) = extends_name {
127 let mut blocks: BTreeMap<String, NodeList> = BTreeMap::new();
129 for node in nodes {
130 if let Node::Block(block) = node {
131 blocks.insert(block.name.clone(), block.body.clone());
132 }
133 }
134
135 let parent = self
137 .templates
138 .get(parent_name.as_str())
139 .ok_or_else(|| Error::undefined_template(&parent_name))?;
140
141 let mut merged_chain: BTreeMap<String, Vec<NodeList>> = if let Some(pb) = parent_blocks {
145 pb.clone()
146 } else {
147 BTreeMap::new()
148 };
149 for (name, body) in &blocks {
150 match merged_chain.get_mut(name) {
151 Some(chain) => chain.push(body.clone()),
152 None => {
153 merged_chain.insert(name.clone(), vec![body.clone()]);
154 }
155 }
156 }
157
158 self.render_with_extends(&parent.nodes, variables, output, Some(&merged_chain), extend_depth + 1)?;
160 } else {
161 let mut renderer = Renderer::new(self, output, variables.clone());
163
164 if let Some(blocks) = parent_blocks {
165 for (name, chain) in blocks {
166 let mut full_chain = chain.clone();
167 if let Some(current_block) = nodes.iter().find_map(|n| {
168 if let Node::Block(b) = n {
169 if b.name == *name { Some(b.body.clone()) } else { None }
170 } else {
171 None
172 }
173 }) {
174 full_chain.push(current_block);
175 }
176 renderer.block_overrides.insert(name.clone(), full_chain);
177 }
178 }
179
180 renderer.render_nodes(nodes)?;
181 }
182
183 Ok(())
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use serde::Serialize;
190
191 use super::*;
192 use crate::context;
193
194 #[test]
195 fn test_simple_template() {
196 let mut engine = Engine::new(EscapeMode::Text);
197 engine.add_template("hello", "Hello, {{ name }}!").unwrap();
198 let result = engine.render("hello", context! { name: "World" }).unwrap();
199 assert_eq!(result, "Hello, World!");
200 }
201
202 #[test]
203 fn test_html_escape() {
204 let mut engine = Engine::new(EscapeMode::Html);
205 engine.add_template("t", "<p>{{ content }}</p>").unwrap();
206 let result = engine
207 .render("t", context! { content: "<script>alert('xss')</script>" })
208 .unwrap();
209 assert_eq!(result, "<p><script>alert('xss')</script></p>");
210 }
211
212 #[test]
213 fn test_text_no_escape() {
214 let mut engine = Engine::new(EscapeMode::Text);
215 engine.add_template("t", "{{ content }}").unwrap();
216 let result = engine
217 .render("t", context! { content: "<script>alert('xss')</script>" })
218 .unwrap();
219 assert_eq!(result, "<script>alert('xss')</script>");
220 }
221
222 #[test]
223 fn test_if_true() {
224 let mut engine = Engine::new(EscapeMode::Text);
225 engine.add_template("t", "{% if show %}visible{% endif %}").unwrap();
226 let result = engine.render("t", context! { show: true }).unwrap();
227 assert_eq!(result, "visible");
228 }
229
230 #[test]
231 fn test_if_false() {
232 let mut engine = Engine::new(EscapeMode::Text);
233 engine.add_template("t", "{% if show %}visible{% endif %}").unwrap();
234 let result = engine.render("t", context! { show: false }).unwrap();
235 assert_eq!(result, "");
236 }
237
238 #[test]
239 fn test_if_else() {
240 let mut engine = Engine::new(EscapeMode::Text);
241 engine
242 .add_template("t", "{% if show %}yes{% else %}no{% endif %}")
243 .unwrap();
244 let result = engine.render("t", context! { show: false }).unwrap();
245 assert_eq!(result, "no");
246 }
247
248 #[test]
249 fn test_if_elif_else() {
250 let mut engine = Engine::new(EscapeMode::Text);
251 engine
252 .add_template("t", "{% if x == 1 %}one{% elif x == 2 %}two{% else %}other{% endif %}")
253 .unwrap();
254 assert_eq!(engine.render("t", context! { x: 1 }).unwrap(), "one");
255 assert_eq!(engine.render("t", context! { x: 2 }).unwrap(), "two");
256 assert_eq!(engine.render("t", context! { x: 3 }).unwrap(), "other");
257 }
258
259 #[test]
260 fn test_for_loop() {
261 let mut engine = Engine::new(EscapeMode::Text);
262 engine
263 .add_template("t", "{% for item in items %}{{ item }},{% endfor %}")
264 .unwrap();
265 let result = engine.render("t", context! { items: vec!["a", "b", "c"] }).unwrap();
266 assert_eq!(result, "a,b,c,");
267 }
268
269 #[test]
270 fn test_dotted_access() {
271 let mut engine = Engine::new(EscapeMode::Text);
272 engine.add_template("t", "{{ user.name }}").unwrap();
273 let result = engine
274 .render("t", context! { user: context! { name: "Alice" } })
275 .unwrap();
276 assert_eq!(result, "Alice");
277 }
278
279 #[test]
280 fn test_filter_upper() {
281 let mut engine = Engine::new(EscapeMode::Text);
282 engine.add_template("t", "{{ name | upper }}").unwrap();
283 let result = engine.render("t", context! { name: "hello" }).unwrap();
284 assert_eq!(result, "HELLO");
285 }
286
287 #[test]
288 fn test_filter_chain() {
289 let mut engine = Engine::new(EscapeMode::Text);
290 engine.add_template("t", "{{ name | upper | reverse }}").unwrap();
291 let result = engine.render("t", context! { name: "abc" }).unwrap();
292 assert_eq!(result, "CBA");
293 }
294
295 #[test]
296 fn test_set_variable() {
297 let mut engine = Engine::new(EscapeMode::Text);
298 engine.add_template("t", "{% set x = 42 %}{{ x }}").unwrap();
299 let result = engine.render("t", context! {}).unwrap();
300 assert_eq!(result, "42");
301 }
302
303 #[test]
304 fn test_raw_block() {
305 let mut engine = Engine::new(EscapeMode::Text);
306 engine
307 .add_template("t", "before{% raw %}{{ not processed }}{% endraw %}after")
308 .unwrap();
309 let result = engine.render("t", context! {}).unwrap();
310 assert_eq!(result, "before{{ not processed }}after");
311 }
312
313 #[test]
314 fn test_include() {
315 let mut engine = Engine::new(EscapeMode::Text);
316 engine.add_template("header", "Header").unwrap();
317 engine.add_template("page", "{% include \"header\" %}Body").unwrap();
318 let result = engine.render("page", context! {}).unwrap();
319 assert_eq!(result, "HeaderBody");
320 }
321
322 #[test]
323 fn test_comparisons() {
324 let mut engine = Engine::new(EscapeMode::Text);
325 engine.add_template("t", "{% if x == 1 %}eq{% endif %}").unwrap();
326 assert_eq!(engine.render("t", context! { x: 1 }).unwrap(), "eq");
327 assert_eq!(engine.render("t", context! { x: 2 }).unwrap(), "");
328 }
329
330 #[test]
331 fn test_not_operator() {
332 let mut engine = Engine::new(EscapeMode::Text);
333 engine.add_template("t", "{% if not x %}empty{% endif %}").unwrap();
334 let result = engine.render("t", context! { x: false }).unwrap();
335 assert_eq!(result, "empty");
336 }
337
338 #[test]
339 fn test_in_operator() {
340 let mut engine = Engine::new(EscapeMode::Text);
341 engine
342 .add_template("t", "{% if \"a\" in items %}found{% endif %}")
343 .unwrap();
344 let result = engine.render("t", context! { items: vec!["a", "b", "c"] }).unwrap();
345 assert_eq!(result, "found");
346 }
347
348 #[test]
349 fn test_nested_if_for() {
350 let mut engine = Engine::new(EscapeMode::Text);
351 engine
352 .add_template(
353 "t",
354 "{% for item in items %}{% if item.active %}{{ item.name }}{% endif %}{% endfor %}",
355 )
356 .unwrap();
357 let result = engine
358 .render(
359 "t",
360 context! {
361 items: vec![
362 context! { name: "a", active: true },
363 context! { name: "b", active: false },
364 context! { name: "c", active: true },
365 ]
366 },
367 )
368 .unwrap();
369 assert_eq!(result, "ac");
370 }
371
372 #[test]
373 fn test_extends_and_blocks() {
374 let mut engine = Engine::new(EscapeMode::Text);
375 engine
376 .add_template("base", "before{% block content %}default{% endblock %}after")
377 .unwrap();
378 engine
379 .add_template("child", "{% extends \"base\" %}{% block content %}child content{% endblock %}")
380 .unwrap();
381 let result = engine.render("child", context! {}).unwrap();
382 assert_eq!(result, "beforechild contentafter");
383 }
384
385 #[test]
386 fn test_super_in_block() {
387 let mut engine = Engine::new(EscapeMode::Text);
388 engine
389 .add_template("base", "{% block content %}parent{% endblock %}")
390 .unwrap();
391 engine
392 .add_template(
393 "child",
394 "{% extends \"base\" %}{% block content %}{{ super() }} + child{% endblock %}",
395 )
396 .unwrap();
397 let result = engine.render("child", context! {}).unwrap();
398 assert_eq!(result, "parent + child");
399 }
400
401 #[test]
402 fn test_comment_is_ignored() {
403 let mut engine = Engine::new(EscapeMode::Text);
404 engine.add_template("t", "before{# comment #}after").unwrap();
405 let result = engine.render("t", context! {}).unwrap();
406 assert_eq!(result, "beforeafter");
407 }
408
409 #[test]
410 fn test_empty_variable() {
411 let mut engine = Engine::new(EscapeMode::Text);
412 engine.add_template("t", "{{ missing }}").unwrap();
413 let result = engine.render("t", context! {}).unwrap();
414 assert_eq!(result, "");
415 }
416
417 #[test]
418 fn test_struct_variables() {
419 #[derive(Serialize)]
420 struct User {
421 name: String,
422 age: i32,
423 }
424
425 let mut engine = Engine::new(EscapeMode::Text);
426 engine.add_template("t", "{{ name }} is {{ age }}").unwrap();
427 let user = User {
428 name: "Bob".into(),
429 age: 30,
430 };
431 let result = engine.render("t", &user).unwrap();
432 assert_eq!(result, "Bob is 30");
433 }
434
435 #[test]
436 fn test_default_filter() {
437 let mut engine = Engine::new(EscapeMode::Text);
438 engine.add_template("t", "{{ name | default(\"unknown\") }}").unwrap();
439
440 let result = engine.render("t", context! { name: "Alice" }).unwrap();
442 assert_eq!(result, "Alice");
443
444 let result = engine.render("t", context! {}).unwrap();
446 assert_eq!(result, "unknown");
447 }
448
449 #[test]
450 fn test_length_filter() {
451 let mut engine = Engine::new(EscapeMode::Text);
452 engine.add_template("t", "{{ items | length }}").unwrap();
453 let result = engine.render("t", context! { items: vec![1, 2, 3] }).unwrap();
454 assert_eq!(result, "3");
455 }
456
457 #[test]
458 fn test_arithmetic() {
459 let mut engine = Engine::new(EscapeMode::Text);
460 engine.add_template("t", "{{ 1 + 2 * 3 }}").unwrap();
461 let result = engine.render("t", context! {}).unwrap();
462 assert_eq!(result, "7");
463 }
464
465 #[test]
466 fn test_float_arithmetic() {
467 let mut engine = Engine::new(EscapeMode::Text);
468 engine.add_template("t", "{{ 3.5 + 2.5 }}").unwrap();
469 let result = engine.render("t", context! {}).unwrap();
470 assert_eq!(result, "6");
471 }
472
473 #[test]
474 fn test_float_division() {
475 let mut engine = Engine::new(EscapeMode::Text);
476 engine.add_template("t", "{{ 10.0 / 3 }}").unwrap();
477 let result = engine.render("t", context! {}).unwrap();
478 assert!(
479 result == "3.3333333333333335" || result == "3.333333333333333",
480 "unexpected float division result: {result}"
481 );
482 }
483
484 #[test]
485 fn test_super_in_html_mode() {
486 let mut engine = Engine::new(EscapeMode::Html);
487 engine
488 .add_template("base", "{% block content %}<b>parent</b>{% endblock %}")
489 .unwrap();
490 engine
491 .add_template(
492 "child",
493 "{% extends \"base\" %}{% block content %}{{ super() }}<i>child</i>{% endblock %}",
494 )
495 .unwrap();
496 let result = engine.render("child", context! {}).unwrap();
497 assert_eq!(result, "<b>parent</b><i>child</i>");
498 }
499
500 #[test]
501 fn test_multi_level_extends() {
502 let mut engine = Engine::new(EscapeMode::Text);
503 engine
504 .add_template("base", "{% block content %}base{% endblock %}")
505 .unwrap();
506 engine
507 .add_template(
508 "child",
509 "{% extends \"base\" %}{% block content %}child {{ super() }}{% endblock %}",
510 )
511 .unwrap();
512 engine
513 .add_template(
514 "grandchild",
515 "{% extends \"child\" %}{% block content %}grandchild {{ super() }}{% endblock %}",
516 )
517 .unwrap();
518 let result = engine.render("grandchild", context! {}).unwrap();
519 assert_eq!(result, "grandchild child base");
520 }
521
522 #[test]
523 fn test_safe_filter_html_mode() {
524 let mut engine = Engine::new(EscapeMode::Html);
525 engine.add_template("t", "{{ content | safe }}").unwrap();
526 let result = engine.render("t", context! { content: "<b>bold</b>" }).unwrap();
527 assert_eq!(result, "<b>bold</b>");
528 }
529
530 #[test]
531 fn test_escape_filter_html_mode() {
532 let mut engine = Engine::new(EscapeMode::Html);
533 engine
534 .add_template("t", "{{ content }} and {{ content | escape }}")
535 .unwrap();
536 let result = engine.render("t", context! { content: "<br>" }).unwrap();
537 assert_eq!(result, "<br> and <br>");
539 }
540
541 #[test]
542 fn test_for_loop_over_string() {
543 let mut engine = Engine::new(EscapeMode::Text);
544 engine
545 .add_template("t", "{% for c in s %}{{ c }}|{% endfor %}")
546 .unwrap();
547 let result = engine.render("t", context! { s: "ab" }).unwrap();
548 assert_eq!(result, "a|b|");
549 }
550
551 #[test]
552 fn test_index_access() {
553 let mut engine = Engine::new(EscapeMode::Text);
554 engine.add_template("t", "{{ items[0] }},{{ items[1] }}").unwrap();
555 let result = engine.render("t", context! { items: vec!["a", "b"] }).unwrap();
556 assert_eq!(result, "a,b");
557 }
558
559 #[test]
560 fn test_in_operator_string() {
561 let mut engine = Engine::new(EscapeMode::Text);
562 engine
563 .add_template("t", "{% if \"world\" in \"hello world\" %}found{% endif %}")
564 .unwrap();
565 let result = engine.render("t", context! {}).unwrap();
566 assert_eq!(result, "found");
567 }
568
569 #[test]
570 fn test_missing_endif_errors() {
571 let mut engine = Engine::new(EscapeMode::Text);
572 let result = engine.add_template("t", "{% if true %}hello");
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn test_length_filter_map() {
578 let mut engine = Engine::new(EscapeMode::Text);
579 engine.add_template("t", "{{ obj | length }}").unwrap();
580 let result = engine.render("t", context! { obj: context! { a: 1, b: 2 } }).unwrap();
581 assert_eq!(result, "2");
582 }
583
584 #[test]
585 fn test_undefined_template_error() {
586 let engine = Engine::new(EscapeMode::Text);
587 let result = engine.render("nonexistent", context! {});
588 assert!(result.is_err());
589 }
590
591 #[test]
592 fn test_division_by_zero() {
593 let mut engine = Engine::new(EscapeMode::Text);
594 engine.add_template("t", "{{ 1 / 0 }}").unwrap();
595 let result = engine.render("t", context! {});
596 assert!(result.is_err());
597 }
598
599 #[test]
600 fn test_modulo_by_zero() {
601 let mut engine = Engine::new(EscapeMode::Text);
602 engine.add_template("t", "{{ 10 % 0 }}").unwrap();
603 let result = engine.render("t", context! {});
604 assert!(result.is_err());
605 }
606
607 #[test]
608 fn test_circular_include() {
609 let mut engine = Engine::new(EscapeMode::Text);
610 engine.add_template("a", "{% include \"b\" %}").unwrap();
611 engine.add_template("b", "{% include \"a\" %}").unwrap();
612 let result = engine.render("a", context! {});
613 assert!(result.is_err());
614 }
615
616 #[test]
617 fn test_circular_extends() {
618 let mut engine = Engine::new(EscapeMode::Text);
619 engine
620 .add_template("a", "{% extends \"b\" %}{% block x %}a{% endblock %}")
621 .unwrap();
622 engine
623 .add_template("b", "{% extends \"a\" %}{% block x %}b{% endblock %}")
624 .unwrap();
625 let result = engine.render("a", context! {});
626 assert!(result.is_err());
627 }
628
629 #[test]
630 fn test_unknown_function() {
631 let mut engine = Engine::new(EscapeMode::Text);
632 engine.add_template("t", "{{ foobar() }}").unwrap();
633 let result = engine.render("t", context! {});
634 assert!(result.is_err());
635 }
636
637 #[test]
638 fn test_range_with_float() {
639 let mut engine = Engine::new(EscapeMode::Text);
640 engine
641 .add_template("t", "{% for i in range(5.5) %}{{ i }}{% endfor %}")
642 .unwrap();
643 let result = engine.render("t", context! {});
644 assert!(result.is_err());
645 }
646
647 #[test]
648 fn test_trailing_tokens_in_expr() {
649 let mut engine = Engine::new(EscapeMode::Text);
650 let result = engine.add_template("t", "{{ true false }}");
651 assert!(result.is_err());
652 }
653
654 #[test]
655 fn test_trailing_tokens_after_filter() {
656 let mut engine = Engine::new(EscapeMode::Text);
657 let result = engine.add_template("t", "{{ name | upper + 1 }}");
658 assert!(result.is_err());
659 }
660
661 #[test]
662 fn test_add_template_overwrite_error() {
663 let mut engine = Engine::new(EscapeMode::Text);
664 engine.add_template("t", "hello").unwrap();
665 let result = engine.add_template("t", "world");
666 assert!(result.is_err());
667 }
668
669 #[test]
670 fn test_raw_block_preserves_inner_tags() {
671 let mut engine = Engine::new(EscapeMode::Text);
672 engine
673 .add_template("t", "before{% raw %}{% inner %}{% endraw %}after")
674 .unwrap();
675 let result = engine.render("t", context! {}).unwrap();
676 assert_eq!(result, "before{% inner %}after");
677 }
678
679 #[test]
680 fn test_raw_block_preserves_trailing_whitespace() {
681 let mut engine = Engine::new(EscapeMode::Text);
682 engine
683 .add_template("t", "before{% raw %}hello {% endraw %}after")
684 .unwrap();
685 let result = engine.render("t", context! {}).unwrap();
686 assert_eq!(result, "beforehello after");
687 }
688
689 #[test]
690 fn test_range_with_integer() {
691 let mut engine = Engine::new(EscapeMode::Text);
692 engine
693 .add_template("t", "{% for i in range(3) %}{{ i }}{% endfor %}")
694 .unwrap();
695 let result = engine.render("t", context! {}).unwrap();
696 assert_eq!(result, "012");
697 }
698
699 #[test]
700 fn test_length_filter_in_html_mode_on_safe() {
701 let mut engine = Engine::new(EscapeMode::Html);
702 engine.add_template("t", "{{ \"hello\" | escape | length }}").unwrap();
703 let result = engine.render("t", context! {}).unwrap();
704 assert_eq!(result, "5");
706 }
707
708 #[test]
709 fn test_first_on_safe_string() {
710 let mut engine = Engine::new(EscapeMode::Text);
711 engine.add_template("t", "{{ \"abc\" | safe | first }}").unwrap();
712 let result = engine.render("t", context! {}).unwrap();
713 assert_eq!(result, "a");
714 }
715
716 #[test]
717 fn test_last_on_safe_string() {
718 let mut engine = Engine::new(EscapeMode::Text);
719 engine.add_template("t", "{{ \"abc\" | safe | last }}").unwrap();
720 let result = engine.render("t", context! {}).unwrap();
721 assert_eq!(result, "c");
722 }
723
724 #[test]
725 fn test_reverse_on_safe_string() {
726 let mut engine = Engine::new(EscapeMode::Text);
727 engine.add_template("t", "{{ \"abc\" | safe | reverse }}").unwrap();
728 let result = engine.render("t", context! {}).unwrap();
729 assert_eq!(result, "cba");
730 }
731
732 #[test]
733 fn test_length_on_safe_string() {
734 let mut engine = Engine::new(EscapeMode::Text);
735 engine.add_template("t", "{{ \"hello\" | safe | length }}").unwrap();
736 let result = engine.render("t", context! {}).unwrap();
737 assert_eq!(result, "5");
738 }
739
740 #[test]
741 fn test_short_circuit_and() {
742 let mut engine = Engine::new(EscapeMode::Text);
743 engine.add_template("t", "{% if false and 1/0 %}ok{% endif %}").unwrap();
744 let result = engine.render("t", context! {}).unwrap();
745 assert_eq!(result, "");
746 }
747
748 #[test]
749 fn test_short_circuit_or() {
750 let mut engine = Engine::new(EscapeMode::Text);
751 engine.add_template("t", "{% if true or 1/0 %}ok{% endif %}").unwrap();
752 let result = engine.render("t", context! {}).unwrap();
753 assert_eq!(result, "ok");
754 }
755
756 #[test]
757 fn test_range_no_args() {
758 let mut engine = Engine::new(EscapeMode::Text);
759 engine.add_template("t", "{{ range() }}").unwrap();
760 let result = engine.render("t", context! {});
761 assert!(result.is_err());
762 }
763
764 #[test]
765 fn test_range_extra_args() {
766 let mut engine = Engine::new(EscapeMode::Text);
767 engine
768 .add_template("t", "{% for i in range(1,2,3) %}{{ i }}{% endfor %}")
769 .unwrap();
770 let result = engine.render("t", context! {});
771 assert!(result.is_err());
772 }
773
774 #[test]
775 fn test_for_empty_string() {
776 let mut engine = Engine::new(EscapeMode::Text);
777 engine
778 .add_template("t", "{% for c in \"\" %}{{ c }}{% endfor %}")
779 .unwrap();
780 let result = engine.render("t", context! {}).unwrap();
781 assert_eq!(result, "");
782 }
783
784 #[test]
785 fn test_for_empty_array() {
786 let mut engine = Engine::new(EscapeMode::Text);
787 engine
788 .add_template("t", "{% for i in items %}{{ i }}{% endfor %}")
789 .unwrap();
790 let result = engine.render("t", context! { items: Vec::<i32>::new() }).unwrap();
791 assert_eq!(result, "");
792 }
793
794 #[test]
795 fn test_for_over_number() {
796 let mut engine = Engine::new(EscapeMode::Text);
797 engine
798 .add_template("t", "{% for i in 42 %}{{ i }}{% endfor %}")
799 .unwrap();
800 let result = engine.render("t", context! {}).unwrap();
801 assert_eq!(result, "");
802 }
803
804 #[test]
805 fn test_for_over_map() {
806 let mut engine = Engine::new(EscapeMode::Text);
807 engine.add_template("t", "{% for i in m %}{{ i }}{% endfor %}").unwrap();
808 let result = engine.render("t", context! { m: context! { a: 1 } }).unwrap();
809 assert_eq!(result, "");
810 }
811
812 #[test]
813 fn test_missing_include() {
814 let mut engine = Engine::new(EscapeMode::Text);
815 engine.add_template("t", "{% include \"missing\" %}").unwrap();
816 let result = engine.render("t", context! {});
817 assert!(result.is_err());
818 }
819
820 #[test]
821 fn test_missing_extends() {
822 let mut engine = Engine::new(EscapeMode::Text);
823 engine.add_template("t", "{% extends \"missing\" %}").unwrap();
824 let result = engine.render("t", context! {});
825 assert!(result.is_err());
826 }
827
828 #[test]
829 fn test_super_without_parent() {
830 let mut engine = Engine::new(EscapeMode::Text);
831 engine.add_template("t", "{{ super() }}").unwrap();
832 let result = engine.render("t", context! {}).unwrap();
833 assert_eq!(result, "");
834 }
835
836 #[test]
837 fn test_set_overwrite() {
838 let mut engine = Engine::new(EscapeMode::Text);
839 engine
840 .add_template("t", "{% set x = 1 %}{% set x = 2 %}{{ x }}")
841 .unwrap();
842 let result = engine.render("t", context! {}).unwrap();
843 assert_eq!(result, "2");
844 }
845
846 #[test]
847 fn test_nested_missing() {
848 let mut engine = Engine::new(EscapeMode::Text);
849 engine.add_template("t", "{{ a.b.c }}").unwrap();
850 let result = engine.render("t", context! {}).unwrap();
851 assert_eq!(result, "");
852 }
853
854 #[test]
855 fn test_negative_index_runtime() {
856 let mut engine = Engine::new(EscapeMode::Text);
857 engine.add_template("t", "{{ items[i] }}").unwrap();
858 let result = engine.render("t", context! { items: vec![1, 2], i: -1 });
859 assert!(result.is_err());
860 }
861
862 #[test]
863 fn test_float_index_runtime() {
864 let mut engine = Engine::new(EscapeMode::Text);
865 engine.add_template("t", "{{ items[i] }}").unwrap();
866 let result = engine.render("t", context! { items: vec![1, 2], i: 1.5 });
867 assert!(result.is_err());
868 }
869
870 #[test]
871 fn test_empty_map_index() {
872 let mut engine = Engine::new(EscapeMode::Text);
873 engine.add_template("t", "{{ m.key }}").unwrap();
874 let result = engine.render("t", context! {}).unwrap();
875 assert_eq!(result, "");
876 }
877
878 #[test]
879 fn test_float_div_by_zero_inf() {
880 let mut engine = Engine::new(EscapeMode::Text);
881 engine.add_template("t", "{{ 1.0 / 0.0 }}").unwrap();
882 let result = engine.render("t", context! {}).unwrap();
883 assert_eq!(result, "inf");
884 }
885
886 #[test]
887 fn test_neg_float_div_by_zero_neg_inf() {
888 let mut engine = Engine::new(EscapeMode::Text);
889 engine.add_template("t", "{{ -1.0 / 0.0 }}").unwrap();
890 let result = engine.render("t", context! {}).unwrap();
891 assert_eq!(result, "-inf");
892 }
893
894 #[test]
895 fn test_float_zero_div_by_zero_nan() {
896 let mut engine = Engine::new(EscapeMode::Text);
897 engine.add_template("t", "{{ 0.0 / 0.0 }}").unwrap();
898 let result = engine.render("t", context! {}).unwrap();
899 assert_eq!(result, "NaN");
900 }
901
902 #[test]
903 fn test_elif_after_else_error() {
904 let mut engine = Engine::new(EscapeMode::Text);
905 let result = engine.add_template("t", "{% if a %}b{% else %}c{% elif d %}e{% endif %}");
906 assert!(result.is_err());
907 }
908
909 #[test]
910 fn test_multiple_else_error() {
911 let mut engine = Engine::new(EscapeMode::Text);
912 let result = engine.add_template("t", "{% if a %}b{% else %}c{% else %}d{% endif %}");
913 assert!(result.is_err());
914 }
915
916 #[test]
917 fn test_empty_if_condition_error() {
918 let mut engine = Engine::new(EscapeMode::Text);
919 let result = engine.add_template("t", "{% if %}a{% endif %}");
920 assert!(result.is_err());
921 }
922}