Skip to main content

template/
engine.rs

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