Skip to main content

template/
engine.rs

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/// Controls auto-escaping behavior.
21///
22/// - `Html` — auto-escapes `{{ ... }}` output (`&`, `<`, `>`, `"`, `'`)
23/// - `Text` — no escaping, raw output
24#[derive(Clone, Debug)]
25pub enum EscapeMode {
26    Html,
27    Text,
28}
29
30pub(crate) struct ParsedTemplate {
31    pub(crate) nodes: NodeList,
32}
33
34/// A template engine that compiles templates into an AST and renders them.
35///
36/// Templates are added by name and can reference each other via `{% include %}`
37/// and `{% extends %}` / `{% block %}`.
38pub struct Engine {
39    pub(crate) mode: EscapeMode,
40    pub(crate) templates: BTreeMap<String, ParsedTemplate>,
41}
42
43impl Engine {
44    /// Create a new engine with the given escaping mode.
45    pub fn new(mode: EscapeMode) -> Self {
46        Self {
47            mode,
48            templates: BTreeMap::new(),
49        }
50    }
51
52    /// Compile and register a template by name.
53    ///
54    /// Returns an error if the template source contains invalid syntax
55    /// (e.g. unclosed `{{ }}`, mismatched `{% if %}` / `{% endif %}`)
56    /// or if a template with the same name already exists.
57    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    /// Render a named template with the given context variables.
74    ///
75    /// Pass a [`Context`] built with the [`context!`] macro, or a reference to
76    /// any type that implements [`Serialize`](serde::Serialize) (requires the
77    /// `std` feature). Non-map values such as plain strings or numbers produce
78    /// a render error.
79    ///
80    /// ```rust
81    /// use template::{Engine, EscapeMode, context};
82    ///
83    /// let mut engine = Engine::new(EscapeMode::Text);
84    /// engine.add_template("t", "Hello, {{ name }}!").unwrap();
85    /// let result = engine.render("t", context! { name: "World" }).unwrap();
86    /// assert_eq!(result, "Hello, World!");
87    /// ```
88    ///
89    /// Returns an error if the template name is not registered or if an
90    /// expression fails during rendering.
91    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        // Check if this template extends another
118        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            // Collect blocks from this template
128            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            // Get parent template
136            let parent = self
137                .templates
138                .get(parent_name.as_str())
139                .ok_or_else(|| Error::undefined_template(&parent_name))?;
140
141            // Merge current blocks into the parent block chain.
142            // Current blocks that shadow existing chain entries push onto the chain;
143            // new blocks start a fresh chain.
144            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            // Render parent with merged blocks
159            self.render_with_extends(&parent.nodes, variables, output, Some(&merged_chain), extend_depth + 1)?;
160        } else {
161            // No extends - render normally with block overrides
162            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>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</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        // With variable set
441        let result = engine.render("t", context! { name: "Alice" }).unwrap();
442        assert_eq!(result, "Alice");
443
444        // Without variable
445        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        // Already auto-escaped in HTML mode, |escape should not double-escape
538        assert_eq!(result, "&lt;br&gt; and &lt;br&gt;");
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        // "hello" escaped is "hello" (no HTML chars), and length is 5 chars
705        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}