1use alloc::{
2 collections::BTreeMap,
3 format,
4 string::{String, ToString},
5 sync::Arc,
6 vec,
7 vec::Vec,
8};
9use core::fmt;
10
11use crate::{
12 ast::{Node, NodeList},
13 context::{Context, IntoContext},
14 error::Error,
15 parser::TemplateParser,
16 value::Value,
17 vm::Renderer,
18};
19
20const MAX_EXTEND_DEPTH: usize = 128;
21
22pub const DEFAULT_RANGE_LIMIT: usize = 32_000;
27
28#[derive(Clone, Debug)]
33pub enum EscapeMode {
34 Html,
35 Text,
36}
37
38pub(crate) struct ParsedTemplate {
39 pub(crate) nodes: NodeList,
40}
41
42pub struct Engine {
55 pub(crate) mode: EscapeMode,
56 pub(crate) templates: BTreeMap<String, ParsedTemplate>,
57 pub(crate) range_limit: usize,
58}
59
60impl Engine {
61 pub fn new(mode: EscapeMode) -> Self {
63 Self {
64 mode,
65 templates: BTreeMap::new(),
66 range_limit: DEFAULT_RANGE_LIMIT,
67 }
68 }
69
70 pub fn set_range_limit(&mut self, limit: usize) {
75 self.range_limit = limit;
76 }
77
78 pub fn range_limit(&self) -> usize {
80 self.range_limit
81 }
82
83 pub fn add_template(&mut self, name: &str, source: &str) -> Result<(), Error> {
90 if self.templates.contains_key(name) {
91 return Err(Error::parse(format!("template `{name}` already exists")));
92 }
93 let mut parser = TemplateParser::new(source);
94 let nodes = parser.parse()?;
95
96 self.templates.insert(
97 name.to_string(),
98 ParsedTemplate {
99 nodes,
100 },
101 );
102 Ok(())
103 }
104
105 pub fn render(&self, name: &str, context: impl IntoContext) -> Result<String, Error> {
124 let mut output = String::new();
125 self.render_to(name, context, &mut output)?;
126 Ok(output)
127 }
128
129 pub fn render_to(&self, name: &str, context: impl IntoContext, out: &mut impl fmt::Write) -> Result<(), Error> {
135 let template = self
136 .templates
137 .get(name)
138 .ok_or_else(|| Error::undefined_template(name))?;
139
140 let context: Context = context.into_context().map_err(|e| Error::parse(e.0))?;
141
142 self.render_with_extends(&template.nodes, &context.0, out, None, 0)
143 }
144
145 fn render_with_extends(
146 &self,
147 nodes: &NodeList,
148 variables: &Value,
149 output: &mut dyn fmt::Write,
150 parent_blocks: Option<&BTreeMap<String, Vec<Arc<NodeList>>>>,
151 extend_depth: usize,
152 ) -> Result<(), Error> {
153 if extend_depth >= MAX_EXTEND_DEPTH {
154 return Err(Error::render(format!("extend depth limit ({MAX_EXTEND_DEPTH}) exceeded")));
155 }
156 let extends_name = nodes.iter().find_map(|n| {
158 if let Node::Extends(name) = n {
159 Some(name.clone())
160 } else {
161 None
162 }
163 });
164
165 if let Some(parent_name) = extends_name {
166 let mut blocks: BTreeMap<String, Arc<NodeList>> = BTreeMap::new();
168 for node in nodes {
169 if let Node::Block(block) = node {
170 blocks.insert(block.name.clone(), Arc::clone(&block.body));
171 }
172 }
173
174 let parent = self
176 .templates
177 .get(parent_name.as_str())
178 .ok_or_else(|| Error::undefined_template(&parent_name))?;
179
180 let mut merged_chain: BTreeMap<String, Vec<Arc<NodeList>>> = parent_blocks.cloned().unwrap_or_default();
184 for (name, body) in &blocks {
185 match merged_chain.get_mut(name) {
186 Some(chain) => chain.push(Arc::clone(body)),
187 None => {
188 merged_chain.insert(name.clone(), vec![Arc::clone(body)]);
189 }
190 }
191 }
192
193 self.render_with_extends(&parent.nodes, variables, output, Some(&merged_chain), extend_depth + 1)?;
195 } else {
196 let mut renderer = Renderer::new(self, output, variables.clone());
198
199 if let Some(blocks) = parent_blocks {
200 for (name, chain) in blocks {
201 let mut full_chain = chain.clone();
202 if let Some(current_block) = nodes.iter().find_map(|n| {
203 if let Node::Block(b) = n {
204 if b.name == *name {
205 Some(Arc::clone(&b.body))
206 } else {
207 None
208 }
209 } else {
210 None
211 }
212 }) {
213 full_chain.push(current_block);
214 }
215 renderer.block_overrides.insert(name.clone(), full_chain);
216 }
217 }
218
219 renderer.render_nodes(nodes)?;
220 }
221
222 Ok(())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use serde::Serialize;
229
230 use super::*;
231 use crate::context;
232
233 #[test]
234 fn test_simple_template() {
235 let mut engine = Engine::new(EscapeMode::Text);
236 engine.add_template("hello", "Hello, {{ name }}!").unwrap();
237 let result = engine.render("hello", context! { name: "World" }).unwrap();
238 assert_eq!(result, "Hello, World!");
239 }
240
241 #[test]
242 fn test_html_escape() {
243 let mut engine = Engine::new(EscapeMode::Html);
244 engine.add_template("t", "<p>{{ content }}</p>").unwrap();
245 let result = engine
246 .render("t", context! { content: "<script>alert('xss')</script>" })
247 .unwrap();
248 assert_eq!(result, "<p><script>alert('xss')</script></p>");
249 }
250
251 #[test]
252 fn test_text_no_escape() {
253 let mut engine = Engine::new(EscapeMode::Text);
254 engine.add_template("t", "{{ content }}").unwrap();
255 let result = engine
256 .render("t", context! { content: "<script>alert('xss')</script>" })
257 .unwrap();
258 assert_eq!(result, "<script>alert('xss')</script>");
259 }
260
261 #[test]
262 fn test_if_true() {
263 let mut engine = Engine::new(EscapeMode::Text);
264 engine.add_template("t", "{% if show %}visible{% endif %}").unwrap();
265 let result = engine.render("t", context! { show: true }).unwrap();
266 assert_eq!(result, "visible");
267 }
268
269 #[test]
270 fn test_if_false() {
271 let mut engine = Engine::new(EscapeMode::Text);
272 engine.add_template("t", "{% if show %}visible{% endif %}").unwrap();
273 let result = engine.render("t", context! { show: false }).unwrap();
274 assert_eq!(result, "");
275 }
276
277 #[test]
278 fn test_if_else() {
279 let mut engine = Engine::new(EscapeMode::Text);
280 engine
281 .add_template("t", "{% if show %}yes{% else %}no{% endif %}")
282 .unwrap();
283 let result = engine.render("t", context! { show: false }).unwrap();
284 assert_eq!(result, "no");
285 }
286
287 #[test]
288 fn test_if_elif_else() {
289 let mut engine = Engine::new(EscapeMode::Text);
290 engine
291 .add_template("t", "{% if x == 1 %}one{% elif x == 2 %}two{% else %}other{% endif %}")
292 .unwrap();
293 assert_eq!(engine.render("t", context! { x: 1 }).unwrap(), "one");
294 assert_eq!(engine.render("t", context! { x: 2 }).unwrap(), "two");
295 assert_eq!(engine.render("t", context! { x: 3 }).unwrap(), "other");
296 }
297
298 #[test]
299 fn test_for_loop() {
300 let mut engine = Engine::new(EscapeMode::Text);
301 engine
302 .add_template("t", "{% for item in items %}{{ item }},{% endfor %}")
303 .unwrap();
304 let result = engine.render("t", context! { items: vec!["a", "b", "c"] }).unwrap();
305 assert_eq!(result, "a,b,c,");
306 }
307
308 #[test]
309 fn test_dotted_access() {
310 let mut engine = Engine::new(EscapeMode::Text);
311 engine.add_template("t", "{{ user.name }}").unwrap();
312 let result = engine
313 .render("t", context! { user: context! { name: "Alice" } })
314 .unwrap();
315 assert_eq!(result, "Alice");
316 }
317
318 #[test]
319 fn test_filter_upper() {
320 let mut engine = Engine::new(EscapeMode::Text);
321 engine.add_template("t", "{{ name | upper }}").unwrap();
322 let result = engine.render("t", context! { name: "hello" }).unwrap();
323 assert_eq!(result, "HELLO");
324 }
325
326 #[test]
327 fn test_filter_chain() {
328 let mut engine = Engine::new(EscapeMode::Text);
329 engine.add_template("t", "{{ name | upper | reverse }}").unwrap();
330 let result = engine.render("t", context! { name: "abc" }).unwrap();
331 assert_eq!(result, "CBA");
332 }
333
334 #[test]
335 fn test_set_variable() {
336 let mut engine = Engine::new(EscapeMode::Text);
337 engine.add_template("t", "{% set x = 42 %}{{ x }}").unwrap();
338 let result = engine.render("t", context! {}).unwrap();
339 assert_eq!(result, "42");
340 }
341
342 #[test]
343 fn test_raw_block() {
344 let mut engine = Engine::new(EscapeMode::Text);
345 engine
346 .add_template("t", "before{% raw %}{{ not processed }}{% endraw %}after")
347 .unwrap();
348 let result = engine.render("t", context! {}).unwrap();
349 assert_eq!(result, "before{{ not processed }}after");
350 }
351
352 #[test]
353 fn test_include() {
354 let mut engine = Engine::new(EscapeMode::Text);
355 engine.add_template("header", "Header").unwrap();
356 engine.add_template("page", "{% include \"header\" %}Body").unwrap();
357 let result = engine.render("page", context! {}).unwrap();
358 assert_eq!(result, "HeaderBody");
359 }
360
361 #[test]
362 fn test_comparisons() {
363 let mut engine = Engine::new(EscapeMode::Text);
364 engine.add_template("t", "{% if x == 1 %}eq{% endif %}").unwrap();
365 assert_eq!(engine.render("t", context! { x: 1 }).unwrap(), "eq");
366 assert_eq!(engine.render("t", context! { x: 2 }).unwrap(), "");
367 }
368
369 #[test]
370 fn test_not_operator() {
371 let mut engine = Engine::new(EscapeMode::Text);
372 engine.add_template("t", "{% if not x %}empty{% endif %}").unwrap();
373 let result = engine.render("t", context! { x: false }).unwrap();
374 assert_eq!(result, "empty");
375 }
376
377 #[test]
378 fn test_in_operator() {
379 let mut engine = Engine::new(EscapeMode::Text);
380 engine
381 .add_template("t", "{% if \"a\" in items %}found{% endif %}")
382 .unwrap();
383 let result = engine.render("t", context! { items: vec!["a", "b", "c"] }).unwrap();
384 assert_eq!(result, "found");
385 }
386
387 #[test]
388 fn test_nested_if_for() {
389 let mut engine = Engine::new(EscapeMode::Text);
390 engine
391 .add_template(
392 "t",
393 "{% for item in items %}{% if item.active %}{{ item.name }}{% endif %}{% endfor %}",
394 )
395 .unwrap();
396 let result = engine
397 .render(
398 "t",
399 context! {
400 items: vec![
401 context! { name: "a", active: true },
402 context! { name: "b", active: false },
403 context! { name: "c", active: true },
404 ]
405 },
406 )
407 .unwrap();
408 assert_eq!(result, "ac");
409 }
410
411 #[test]
412 fn test_extends_and_blocks() {
413 let mut engine = Engine::new(EscapeMode::Text);
414 engine
415 .add_template("base", "before{% block content %}default{% endblock %}after")
416 .unwrap();
417 engine
418 .add_template("child", "{% extends \"base\" %}{% block content %}child content{% endblock %}")
419 .unwrap();
420 let result = engine.render("child", context! {}).unwrap();
421 assert_eq!(result, "beforechild contentafter");
422 }
423
424 #[test]
425 fn test_super_in_block() {
426 let mut engine = Engine::new(EscapeMode::Text);
427 engine
428 .add_template("base", "{% block content %}parent{% endblock %}")
429 .unwrap();
430 engine
431 .add_template(
432 "child",
433 "{% extends \"base\" %}{% block content %}{{ super() }} + child{% endblock %}",
434 )
435 .unwrap();
436 let result = engine.render("child", context! {}).unwrap();
437 assert_eq!(result, "parent + child");
438 }
439
440 #[test]
441 fn test_comment_is_ignored() {
442 let mut engine = Engine::new(EscapeMode::Text);
443 engine.add_template("t", "before{# comment #}after").unwrap();
444 let result = engine.render("t", context! {}).unwrap();
445 assert_eq!(result, "beforeafter");
446 }
447
448 #[test]
449 fn test_empty_variable() {
450 let mut engine = Engine::new(EscapeMode::Text);
451 engine.add_template("t", "{{ missing }}").unwrap();
452 let result = engine.render("t", context! {}).unwrap();
453 assert_eq!(result, "");
454 }
455
456 #[cfg(feature = "std")]
457 #[test]
458 fn test_struct_variables() {
459 #[derive(Serialize)]
460 struct User {
461 name: String,
462 age: i32,
463 }
464
465 let mut engine = Engine::new(EscapeMode::Text);
466 engine.add_template("t", "{{ name }} is {{ age }}").unwrap();
467 let user = User {
468 name: "Bob".into(),
469 age: 30,
470 };
471 let result = engine.render("t", &user).unwrap();
472 assert_eq!(result, "Bob is 30");
473 }
474
475 #[test]
476 fn test_default_filter() {
477 let mut engine = Engine::new(EscapeMode::Text);
478 engine.add_template("t", "{{ name | default(\"unknown\") }}").unwrap();
479
480 let result = engine.render("t", context! { name: "Alice" }).unwrap();
482 assert_eq!(result, "Alice");
483
484 let result = engine.render("t", context! {}).unwrap();
486 assert_eq!(result, "unknown");
487 }
488
489 #[test]
490 fn test_length_filter() {
491 let mut engine = Engine::new(EscapeMode::Text);
492 engine.add_template("t", "{{ items | length }}").unwrap();
493 let result = engine.render("t", context! { items: vec![1, 2, 3] }).unwrap();
494 assert_eq!(result, "3");
495 }
496
497 #[test]
498 fn test_arithmetic() {
499 let mut engine = Engine::new(EscapeMode::Text);
500 engine.add_template("t", "{{ 1 + 2 * 3 }}").unwrap();
501 let result = engine.render("t", context! {}).unwrap();
502 assert_eq!(result, "7");
503 }
504
505 #[test]
506 fn test_float_arithmetic() {
507 let mut engine = Engine::new(EscapeMode::Text);
508 engine.add_template("t", "{{ 3.5 + 2.5 }}").unwrap();
509 let result = engine.render("t", context! {}).unwrap();
510 assert_eq!(result, "6");
511 }
512
513 #[test]
514 fn test_float_division() {
515 let mut engine = Engine::new(EscapeMode::Text);
516 engine.add_template("t", "{{ 10.0 / 3 }}").unwrap();
517 let result = engine.render("t", context! {}).unwrap();
518 assert!(
519 result == "3.3333333333333335" || result == "3.333333333333333",
520 "unexpected float division result: {result}"
521 );
522 }
523
524 #[test]
525 fn test_super_in_html_mode() {
526 let mut engine = Engine::new(EscapeMode::Html);
527 engine
528 .add_template("base", "{% block content %}<b>parent</b>{% endblock %}")
529 .unwrap();
530 engine
531 .add_template(
532 "child",
533 "{% extends \"base\" %}{% block content %}{{ super() }}<i>child</i>{% endblock %}",
534 )
535 .unwrap();
536 let result = engine.render("child", context! {}).unwrap();
537 assert_eq!(result, "<b>parent</b><i>child</i>");
538 }
539
540 #[test]
541 fn test_multi_level_extends() {
542 let mut engine = Engine::new(EscapeMode::Text);
543 engine
544 .add_template("base", "{% block content %}base{% endblock %}")
545 .unwrap();
546 engine
547 .add_template(
548 "child",
549 "{% extends \"base\" %}{% block content %}child {{ super() }}{% endblock %}",
550 )
551 .unwrap();
552 engine
553 .add_template(
554 "grandchild",
555 "{% extends \"child\" %}{% block content %}grandchild {{ super() }}{% endblock %}",
556 )
557 .unwrap();
558 let result = engine.render("grandchild", context! {}).unwrap();
559 assert_eq!(result, "grandchild child base");
560 }
561
562 #[test]
563 fn test_safe_filter_html_mode() {
564 let mut engine = Engine::new(EscapeMode::Html);
565 engine.add_template("t", "{{ content | safe }}").unwrap();
566 let result = engine.render("t", context! { content: "<b>bold</b>" }).unwrap();
567 assert_eq!(result, "<b>bold</b>");
568 }
569
570 #[test]
571 fn test_escape_filter_html_mode() {
572 let mut engine = Engine::new(EscapeMode::Html);
573 engine
574 .add_template("t", "{{ content }} and {{ content | escape }}")
575 .unwrap();
576 let result = engine.render("t", context! { content: "<br>" }).unwrap();
577 assert_eq!(result, "<br> and <br>");
579 }
580
581 #[test]
582 fn test_for_loop_over_string() {
583 let mut engine = Engine::new(EscapeMode::Text);
584 engine
585 .add_template("t", "{% for c in s %}{{ c }}|{% endfor %}")
586 .unwrap();
587 let result = engine.render("t", context! { s: "ab" }).unwrap();
588 assert_eq!(result, "a|b|");
589 }
590
591 #[test]
592 fn test_index_access() {
593 let mut engine = Engine::new(EscapeMode::Text);
594 engine.add_template("t", "{{ items[0] }},{{ items[1] }}").unwrap();
595 let result = engine.render("t", context! { items: vec!["a", "b"] }).unwrap();
596 assert_eq!(result, "a,b");
597 }
598
599 #[test]
600 fn test_in_operator_string() {
601 let mut engine = Engine::new(EscapeMode::Text);
602 engine
603 .add_template("t", "{% if \"world\" in \"hello world\" %}found{% endif %}")
604 .unwrap();
605 let result = engine.render("t", context! {}).unwrap();
606 assert_eq!(result, "found");
607 }
608
609 #[test]
610 fn test_missing_endif_errors() {
611 let mut engine = Engine::new(EscapeMode::Text);
612 let result = engine.add_template("t", "{% if true %}hello");
613 assert!(result.is_err());
614 }
615
616 #[test]
617 fn test_length_filter_map() {
618 let mut engine = Engine::new(EscapeMode::Text);
619 engine.add_template("t", "{{ obj | length }}").unwrap();
620 let result = engine.render("t", context! { obj: context! { a: 1, b: 2 } }).unwrap();
621 assert_eq!(result, "2");
622 }
623
624 #[test]
625 fn test_undefined_template_error() {
626 let engine = Engine::new(EscapeMode::Text);
627 let result = engine.render("nonexistent", context! {});
628 assert!(result.is_err());
629 }
630
631 #[test]
632 fn test_division_by_zero() {
633 let mut engine = Engine::new(EscapeMode::Text);
634 engine.add_template("t", "{{ 1 / 0 }}").unwrap();
635 let result = engine.render("t", context! {});
636 assert!(result.is_err());
637 }
638
639 #[test]
640 fn test_modulo_by_zero() {
641 let mut engine = Engine::new(EscapeMode::Text);
642 engine.add_template("t", "{{ 10 % 0 }}").unwrap();
643 let result = engine.render("t", context! {});
644 assert!(result.is_err());
645 }
646
647 #[test]
648 fn test_circular_include() {
649 let mut engine = Engine::new(EscapeMode::Text);
650 engine.add_template("a", "{% include \"b\" %}").unwrap();
651 engine.add_template("b", "{% include \"a\" %}").unwrap();
652 let result = engine.render("a", context! {});
653 assert!(result.is_err());
654 }
655
656 #[test]
657 fn test_circular_extends() {
658 let mut engine = Engine::new(EscapeMode::Text);
659 engine
660 .add_template("a", "{% extends \"b\" %}{% block x %}a{% endblock %}")
661 .unwrap();
662 engine
663 .add_template("b", "{% extends \"a\" %}{% block x %}b{% endblock %}")
664 .unwrap();
665 let result = engine.render("a", context! {});
666 assert!(result.is_err());
667 }
668
669 #[test]
670 fn test_unknown_function() {
671 let mut engine = Engine::new(EscapeMode::Text);
672 engine.add_template("t", "{{ foobar() }}").unwrap();
673 let result = engine.render("t", context! {});
674 assert!(result.is_err());
675 }
676
677 #[test]
678 fn test_range_with_float() {
679 let mut engine = Engine::new(EscapeMode::Text);
680 engine
681 .add_template("t", "{% for i in range(5.5) %}{{ i }}{% endfor %}")
682 .unwrap();
683 let result = engine.render("t", context! {});
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn test_trailing_tokens_in_expr() {
689 let mut engine = Engine::new(EscapeMode::Text);
690 let result = engine.add_template("t", "{{ true false }}");
691 assert!(result.is_err());
692 }
693
694 #[test]
695 fn test_trailing_tokens_after_filter() {
696 let mut engine = Engine::new(EscapeMode::Text);
697 let result = engine.add_template("t", "{{ name | upper + 1 }}");
698 assert!(result.is_err());
699 }
700
701 #[test]
702 fn test_add_template_overwrite_error() {
703 let mut engine = Engine::new(EscapeMode::Text);
704 engine.add_template("t", "hello").unwrap();
705 let result = engine.add_template("t", "world");
706 assert!(result.is_err());
707 }
708
709 #[test]
710 fn test_raw_block_preserves_inner_tags() {
711 let mut engine = Engine::new(EscapeMode::Text);
712 engine
713 .add_template("t", "before{% raw %}{% inner %}{% endraw %}after")
714 .unwrap();
715 let result = engine.render("t", context! {}).unwrap();
716 assert_eq!(result, "before{% inner %}after");
717 }
718
719 #[test]
720 fn test_raw_block_preserves_trailing_whitespace() {
721 let mut engine = Engine::new(EscapeMode::Text);
722 engine
723 .add_template("t", "before{% raw %}hello {% endraw %}after")
724 .unwrap();
725 let result = engine.render("t", context! {}).unwrap();
726 assert_eq!(result, "beforehello after");
727 }
728
729 #[test]
730 fn test_range_with_integer() {
731 let mut engine = Engine::new(EscapeMode::Text);
732 engine
733 .add_template("t", "{% for i in range(3) %}{{ i }}{% endfor %}")
734 .unwrap();
735 let result = engine.render("t", context! {}).unwrap();
736 assert_eq!(result, "012");
737 }
738
739 #[test]
740 fn test_length_filter_in_html_mode_on_safe() {
741 let mut engine = Engine::new(EscapeMode::Html);
742 engine.add_template("t", "{{ \"hello\" | escape | length }}").unwrap();
743 let result = engine.render("t", context! {}).unwrap();
744 assert_eq!(result, "5");
746 }
747
748 #[test]
749 fn test_first_on_safe_string() {
750 let mut engine = Engine::new(EscapeMode::Text);
751 engine.add_template("t", "{{ \"abc\" | safe | first }}").unwrap();
752 let result = engine.render("t", context! {}).unwrap();
753 assert_eq!(result, "a");
754 }
755
756 #[test]
757 fn test_last_on_safe_string() {
758 let mut engine = Engine::new(EscapeMode::Text);
759 engine.add_template("t", "{{ \"abc\" | safe | last }}").unwrap();
760 let result = engine.render("t", context! {}).unwrap();
761 assert_eq!(result, "c");
762 }
763
764 #[test]
765 fn test_reverse_on_safe_string() {
766 let mut engine = Engine::new(EscapeMode::Text);
767 engine.add_template("t", "{{ \"abc\" | safe | reverse }}").unwrap();
768 let result = engine.render("t", context! {}).unwrap();
769 assert_eq!(result, "cba");
770 }
771
772 #[test]
773 fn test_length_on_safe_string() {
774 let mut engine = Engine::new(EscapeMode::Text);
775 engine.add_template("t", "{{ \"hello\" | safe | length }}").unwrap();
776 let result = engine.render("t", context! {}).unwrap();
777 assert_eq!(result, "5");
778 }
779
780 #[test]
781 fn test_short_circuit_and() {
782 let mut engine = Engine::new(EscapeMode::Text);
783 engine.add_template("t", "{% if false and 1/0 %}ok{% endif %}").unwrap();
784 let result = engine.render("t", context! {}).unwrap();
785 assert_eq!(result, "");
786 }
787
788 #[test]
789 fn test_short_circuit_or() {
790 let mut engine = Engine::new(EscapeMode::Text);
791 engine.add_template("t", "{% if true or 1/0 %}ok{% endif %}").unwrap();
792 let result = engine.render("t", context! {}).unwrap();
793 assert_eq!(result, "ok");
794 }
795
796 #[test]
797 fn test_range_no_args() {
798 let mut engine = Engine::new(EscapeMode::Text);
799 engine.add_template("t", "{{ range() }}").unwrap();
800 let result = engine.render("t", context! {});
801 assert!(result.is_err());
802 }
803
804 #[test]
805 fn test_range_extra_args() {
806 let mut engine = Engine::new(EscapeMode::Text);
807 engine
808 .add_template("t", "{% for i in range(1,2,3) %}{{ i }}{% endfor %}")
809 .unwrap();
810 let result = engine.render("t", context! {});
811 assert!(result.is_err());
812 }
813
814 #[test]
815 fn test_for_empty_string() {
816 let mut engine = Engine::new(EscapeMode::Text);
817 engine
818 .add_template("t", "{% for c in \"\" %}{{ c }}{% endfor %}")
819 .unwrap();
820 let result = engine.render("t", context! {}).unwrap();
821 assert_eq!(result, "");
822 }
823
824 #[test]
825 fn test_for_empty_array() {
826 let mut engine = Engine::new(EscapeMode::Text);
827 engine
828 .add_template("t", "{% for i in items %}{{ i }}{% endfor %}")
829 .unwrap();
830 let result = engine.render("t", context! { items: Vec::<i32>::new() }).unwrap();
831 assert_eq!(result, "");
832 }
833
834 #[test]
835 fn test_for_over_number() {
836 let mut engine = Engine::new(EscapeMode::Text);
837 engine
838 .add_template("t", "{% for i in 42 %}{{ i }}{% endfor %}")
839 .unwrap();
840 let result = engine.render("t", context! {}).unwrap();
841 assert_eq!(result, "");
842 }
843
844 #[test]
845 fn test_for_over_map() {
846 let mut engine = Engine::new(EscapeMode::Text);
847 engine.add_template("t", "{% for i in m %}{{ i }}{% endfor %}").unwrap();
848 let result = engine.render("t", context! { m: context! { a: 1 } }).unwrap();
849 assert_eq!(result, "");
850 }
851
852 #[test]
853 fn test_missing_include() {
854 let mut engine = Engine::new(EscapeMode::Text);
855 engine.add_template("t", "{% include \"missing\" %}").unwrap();
856 let result = engine.render("t", context! {});
857 assert!(result.is_err());
858 }
859
860 #[test]
861 fn test_missing_extends() {
862 let mut engine = Engine::new(EscapeMode::Text);
863 engine.add_template("t", "{% extends \"missing\" %}").unwrap();
864 let result = engine.render("t", context! {});
865 assert!(result.is_err());
866 }
867
868 #[test]
869 fn test_super_without_parent() {
870 let mut engine = Engine::new(EscapeMode::Text);
871 engine.add_template("t", "{{ super() }}").unwrap();
872 let result = engine.render("t", context! {}).unwrap();
873 assert_eq!(result, "");
874 }
875
876 #[test]
877 fn test_set_overwrite() {
878 let mut engine = Engine::new(EscapeMode::Text);
879 engine
880 .add_template("t", "{% set x = 1 %}{% set x = 2 %}{{ x }}")
881 .unwrap();
882 let result = engine.render("t", context! {}).unwrap();
883 assert_eq!(result, "2");
884 }
885
886 #[test]
887 fn test_nested_missing() {
888 let mut engine = Engine::new(EscapeMode::Text);
889 engine.add_template("t", "{{ a.b.c }}").unwrap();
890 let result = engine.render("t", context! {}).unwrap();
891 assert_eq!(result, "");
892 }
893
894 #[test]
895 fn test_negative_index_runtime() {
896 let mut engine = Engine::new(EscapeMode::Text);
897 engine.add_template("t", "{{ items[i] }}").unwrap();
898 let result = engine.render("t", context! { items: vec![1, 2], i: -1 });
899 assert!(result.is_err());
900 }
901
902 #[test]
903 fn test_float_index_runtime() {
904 let mut engine = Engine::new(EscapeMode::Text);
905 engine.add_template("t", "{{ items[i] }}").unwrap();
906 let result = engine.render("t", context! { items: vec![1, 2], i: 1.5 });
907 assert!(result.is_err());
908 }
909
910 #[test]
911 fn test_empty_map_index() {
912 let mut engine = Engine::new(EscapeMode::Text);
913 engine.add_template("t", "{{ m.key }}").unwrap();
914 let result = engine.render("t", context! {}).unwrap();
915 assert_eq!(result, "");
916 }
917
918 #[test]
919 fn test_float_div_by_zero_inf() {
920 let mut engine = Engine::new(EscapeMode::Text);
921 engine.add_template("t", "{{ 1.0 / 0.0 }}").unwrap();
922 let result = engine.render("t", context! {}).unwrap();
923 assert_eq!(result, "inf");
924 }
925
926 #[test]
927 fn test_neg_float_div_by_zero_neg_inf() {
928 let mut engine = Engine::new(EscapeMode::Text);
929 engine.add_template("t", "{{ -1.0 / 0.0 }}").unwrap();
930 let result = engine.render("t", context! {}).unwrap();
931 assert_eq!(result, "-inf");
932 }
933
934 #[test]
935 fn test_float_zero_div_by_zero_nan() {
936 let mut engine = Engine::new(EscapeMode::Text);
937 engine.add_template("t", "{{ 0.0 / 0.0 }}").unwrap();
938 let result = engine.render("t", context! {}).unwrap();
939 assert_eq!(result, "NaN");
940 }
941
942 #[test]
943 fn test_elif_after_else_error() {
944 let mut engine = Engine::new(EscapeMode::Text);
945 let result = engine.add_template("t", "{% if a %}b{% else %}c{% elif d %}e{% endif %}");
946 assert!(result.is_err());
947 }
948
949 #[test]
950 fn test_multiple_else_error() {
951 let mut engine = Engine::new(EscapeMode::Text);
952 let result = engine.add_template("t", "{% if a %}b{% else %}c{% else %}d{% endif %}");
953 assert!(result.is_err());
954 }
955
956 #[test]
957 fn test_empty_if_condition_error() {
958 let mut engine = Engine::new(EscapeMode::Text);
959 let result = engine.add_template("t", "{% if %}a{% endif %}");
960 assert!(result.is_err());
961 }
962
963 #[test]
966 fn test_static_non_bool_or_is_parse_error() {
967 let mut engine = Engine::new(EscapeMode::Text);
968 assert!(engine.add_template("t", "{{ 0 or \"x\" }}").is_err());
969 assert!(engine.add_template("t", "{{ \"x\" or true }}").is_err());
970 assert!(engine.add_template("t", "{{ 1 and true }}").is_err());
971 assert!(engine.add_template("t", "{{ not 0 }}").is_err());
972 assert!(engine.add_template("t", "{% if 1 %}a{% endif %}").is_err());
973 assert!(engine.add_template("t", "{% if \"a\" %}a{% endif %}").is_err());
974 }
975
976 #[test]
977 fn test_or_and_return_values() {
978 let mut engine = Engine::new(EscapeMode::Text);
979 engine
980 .add_template(
981 "t",
982 "{{ false or \"x\" }}|{{ true or \"x\" }}|{{ true and \"y\" }}|{{ false and \"y\" }}",
983 )
984 .unwrap();
985 assert_eq!(engine.render("t", context! {}).unwrap(), "x|true|y|false");
987 }
988
989 #[test]
990 fn test_or_chain_flattens() {
991 let mut engine = Engine::new(EscapeMode::Text);
992 engine.add_template("t", "{{ false or false or \"last\" }}").unwrap();
993 assert_eq!(engine.render("t", context! {}).unwrap(), "last");
994 }
995
996 #[test]
997 fn test_dynamic_non_bool_or_is_render_error() {
998 let mut engine = Engine::new(EscapeMode::Text);
999 engine.add_template("t", "{{ x or \"fallback\" }}").unwrap();
1000 assert!(engine.render("t", context! { x: 5 }).is_err());
1001 assert_eq!(engine.render("t", context! { x: false }).unwrap(), "fallback");
1002 assert_eq!(engine.render("t", context! { x: true }).unwrap(), "true");
1003 }
1004
1005 #[test]
1006 fn test_strict_if_condition_rejects_non_bool() {
1007 let mut engine = Engine::new(EscapeMode::Text);
1008 engine.add_template("t", "{% if x %}y{% endif %}").unwrap();
1009 assert!(engine.render("t", context! { x: 0 }).is_err());
1010 assert!(engine.render("t", context! { x: 1 }).is_err());
1011 assert!(engine.render("t", context! { x: "s" }).is_err());
1012 assert!(engine.render("t", context! { x: vec![1] }).is_err());
1013 assert!(engine.render("t", context! {}).is_err());
1015 assert_eq!(engine.render("t", context! { x: true }).unwrap(), "y");
1016 assert_eq!(engine.render("t", context! { x: false }).unwrap(), "");
1017 }
1018
1019 #[test]
1020 fn test_is_set() {
1021 let mut engine = Engine::new(EscapeMode::Text);
1022 engine.add_template("t", "{{ is_set(a.b.c) }}|{{ is_set(x) }}").unwrap();
1023 assert_eq!(engine.render("t", context! {}).unwrap(), "false|false");
1025 assert_eq!(
1026 engine
1027 .render("t", context! { a: context! { b: context! { c: 1 } }, x: 0 })
1028 .unwrap(),
1029 "true|true"
1030 );
1031 assert_eq!(engine.render("t", context! { a: context! { b: 1 } }).unwrap(), "false|false");
1033 }
1034
1035 #[test]
1036 fn test_is_set_wrong_arity() {
1037 let mut engine = Engine::new(EscapeMode::Text);
1038 engine.add_template("t", "{{ is_set() }}").unwrap();
1039 assert!(engine.render("t", context! {}).is_err());
1040 }
1041
1042 #[test]
1045 fn test_integer_overflow_is_error_not_panic() {
1046 let mut engine = Engine::new(EscapeMode::Text);
1047 engine.add_template("t", "{{ x + y }}").unwrap();
1048 assert!(engine.render("t", context! { x: i64::MAX, y: 1i64 }).is_err());
1049 engine.add_template("u", "{{ x * y }}").unwrap();
1050 assert!(engine.render("u", context! { x: i64::MAX, y: 2i64 }).is_err());
1051 engine.add_template("v", "{{ x - y }}").unwrap();
1052 assert!(engine.render("v", context! { x: i64::MIN, y: 1i64 }).is_err());
1053 engine.add_template("w", "{{ -x }}").unwrap();
1054 assert!(engine.render("w", context! { x: i64::MIN }).is_err());
1055 }
1056
1057 #[test]
1058 fn test_min_div_minus_one_is_error() {
1059 let mut engine = Engine::new(EscapeMode::Text);
1061 engine.add_template("d", "{{ x / -1 }}").unwrap();
1062 engine.add_template("m", "{{ x % -1 }}").unwrap();
1063 assert!(engine.render("d", context! { x: i64::MIN }).is_err());
1064 assert!(engine.render("m", context! { x: i64::MIN }).is_err());
1065 }
1066
1067 #[test]
1070 fn test_range_limit_default() {
1071 let mut engine = Engine::new(EscapeMode::Text);
1072 engine.add_template("t", "{{ range(n) | length }}").unwrap();
1073 assert_eq!(engine.render("t", context! { n: 32_000i64 }).unwrap(), "32000");
1074 assert!(engine.render("t", context! { n: 32_001i64 }).is_err());
1075 assert!(engine.render("t", context! { n: i64::MAX }).is_err());
1077 }
1078
1079 #[test]
1080 fn test_range_limit_configurable() {
1081 let mut engine = Engine::new(EscapeMode::Text);
1082 engine.set_range_limit(3);
1083 assert_eq!(engine.range_limit(), 3);
1084 engine.add_template("t", "{{ range(n) | join(\",\") }}").unwrap();
1085 assert_eq!(engine.render("t", context! { n: 3i64 }).unwrap(), "0,1,2");
1086 assert!(engine.render("t", context! { n: 4i64 }).is_err());
1087 }
1088
1089 #[test]
1090 fn test_range_empty_and_reversed() {
1091 let mut engine = Engine::new(EscapeMode::Text);
1092 engine.add_template("t", "{{ range(a, b) | join(\",\") }}").unwrap();
1093 assert_eq!(engine.render("t", context! { a: 5i64, b: 3i64 }).unwrap(), "");
1094 assert_eq!(engine.render("t", context! { a: -2i64, b: 2i64 }).unwrap(), "-2,-1,0,1");
1095 }
1096
1097 #[test]
1100 fn test_template_nesting_limit_returns_error() {
1101 let mut engine = Engine::new(EscapeMode::Text);
1102 let ok = "{% if true %}".repeat(100) + &"{% endif %}".repeat(100);
1103 engine.add_template("ok", &ok).unwrap();
1104 let too_deep = "{% if true %}".repeat(200) + &"{% endif %}".repeat(200);
1105 assert!(engine.add_template("deep", &too_deep).is_err());
1106 }
1107
1108 #[test]
1109 fn test_expression_nesting_limit_returns_error() {
1110 let mut engine = Engine::new(EscapeMode::Text);
1111 let ok = "{{ ".to_string() + &"(".repeat(50) + "1" + &")".repeat(50) + " }}";
1112 engine.add_template("ok", &ok).unwrap();
1113 let too_deep = "{{ ".to_string() + &"(".repeat(200) + "1" + &")".repeat(200) + " }}";
1114 assert!(engine.add_template("deep", &too_deep).is_err());
1115 }
1116
1117 #[test]
1118 fn test_huge_expression_rejected() {
1119 let mut engine = Engine::new(EscapeMode::Text);
1120 let t = format!("{{{{ x{} }}}}", "|upper".repeat(5000));
1122 assert!(engine.add_template("t", &t).is_err());
1123 }
1124
1125 #[test]
1128 fn test_extends_must_be_top_level() {
1129 let mut engine = Engine::new(EscapeMode::Text);
1130 engine.add_template("base", "{% block b %}d{% endblock %}").unwrap();
1131 assert!(
1132 engine
1133 .add_template("c", "{% if true %}{% extends \"base\" %}{% endif %}")
1134 .is_err()
1135 );
1136 }
1137
1138 #[test]
1139 fn test_include_with_extends_is_error() {
1140 let mut engine = Engine::new(EscapeMode::Text);
1141 engine.add_template("base", "BASE{% block b %}d{% endblock %}").unwrap();
1142 engine
1143 .add_template("sub", "{% extends \"base\" %}{% block b %}S{% endblock %}")
1144 .unwrap();
1145 engine.add_template("page", "{% include \"sub\" %}").unwrap();
1146 assert!(engine.render("page", context! {}).is_err());
1147 }
1148
1149 #[test]
1152 fn test_delimiters_inside_string_literals() {
1153 let mut engine = Engine::new(EscapeMode::Text);
1154 engine.add_template("t", "{{ \"a}}b\" }}").unwrap();
1155 assert_eq!(engine.render("t", context! {}).unwrap(), "a}}b");
1156 engine
1157 .add_template("u", "{% if x == \"%}\" %}Y{% else %}N{% endif %}")
1158 .unwrap();
1159 assert_eq!(engine.render("u", context! { x: "%}" }).unwrap(), "Y");
1160 assert_eq!(engine.render("u", context! { x: "nope" }).unwrap(), "N");
1161 }
1162
1163 #[test]
1164 fn test_unterminated_string_is_parse_error() {
1165 let mut engine = Engine::new(EscapeMode::Text);
1166 assert!(engine.add_template("t", "{{ \"abc }}").is_err());
1167 assert!(engine.add_template("u", "{% if x == \"abc %}y{% endif %}").is_err());
1168 }
1169
1170 #[test]
1171 fn test_unknown_filter_is_parse_error() {
1172 let mut engine = Engine::new(EscapeMode::Text);
1173 assert!(engine.add_template("t", "{{ x | nope }}").is_err());
1174 }
1175
1176 #[test]
1177 fn test_wrong_filter_arity_is_parse_error() {
1178 let mut engine = Engine::new(EscapeMode::Text);
1179 assert!(engine.add_template("t", "{{ x | upper(\"junk\") }}").is_err());
1180 assert!(engine.add_template("u", "{{ x | join(\",\", \"extra\") }}").is_err());
1181 assert!(engine.add_template("v", "{{ x | default(1) }}").is_ok());
1183 assert!(engine.add_template("w", "{{ x | upper }}").is_ok());
1184 }
1185
1186 #[test]
1187 fn test_default_is_null_only() {
1188 let mut engine = Engine::new(EscapeMode::Text);
1189 engine
1190 .add_template(
1191 "t",
1192 "{{ z | default(9) }}|{{ f | default(9) }}|{{ s | default(9) }}|{{ m | default(9) }}",
1193 )
1194 .unwrap();
1195 let out = engine.render("t", context! { z: 0, f: false, s: "" }).unwrap();
1196 assert_eq!(out, "0|false||9");
1197 }
1198
1199 #[test]
1202 fn test_safe_survives_pure_filters() {
1203 let mut engine = Engine::new(EscapeMode::Html);
1204 engine.add_template("t", "{{ x | safe | reverse }}").unwrap();
1205 assert_eq!(engine.render("t", context! { x: "<b>" }).unwrap(), ">b<");
1207 }
1208
1209 #[test]
1210 fn test_safe_compares_equal_to_str() {
1211 let mut engine = Engine::new(EscapeMode::Text);
1212 engine
1213 .add_template("t", "{{ (x | safe) == x }}|{{ (x | safe) < \"z\" }}")
1214 .unwrap();
1215 assert_eq!(engine.render("t", context! { x: "a" }).unwrap(), "true|true");
1216 }
1217
1218 #[test]
1219 fn test_join_leaves_unsafe_element_escaped() {
1220 let mut engine = Engine::new(EscapeMode::Html);
1221 engine.add_template("t", "{{ items | join(\",\") }}").unwrap();
1222 let out = engine.render("t", context! { items: vec!["<a>", "<b>"] }).unwrap();
1223 assert_eq!(out, "<a>,<b>");
1224 }
1225
1226 #[test]
1229 fn test_xss_payloads_are_escaped() {
1230 let mut engine = Engine::new(EscapeMode::Html);
1231 engine.add_template("t", "{{ x }}").unwrap();
1232 for payload in [
1233 "<script>alert(1)</script>",
1234 "\" onmouseover=alert(1)",
1235 "' onclick='alert(1)",
1236 "`backtick`",
1237 "&",
1238 "javascript:alert(1)",
1239 ] {
1240 let out = engine.render("t", context! { x: payload }).unwrap();
1241 assert!(!out.contains('<'), "raw `<` leaked for {payload:?}");
1242 assert!(!out.contains('>'), "raw `>` leaked for {payload:?}");
1243 assert!(!out.contains('"'), "raw `\"` leaked for {payload:?}");
1244 assert!(!out.contains('\''), "raw `'` leaked for {payload:?}");
1245 }
1246 }
1247
1248 #[test]
1249 fn test_escape_filter_does_not_double_escape() {
1250 let mut engine = Engine::new(EscapeMode::Html);
1251 engine.add_template("t", "{{ x | escape }}").unwrap();
1252 assert_eq!(engine.render("t", context! { x: "<br>" }).unwrap(), "<br>");
1253 }
1254
1255 #[test]
1258 fn test_render_to_writer() {
1259 use core::fmt;
1260
1261 struct Sink(String);
1262 impl fmt::Write for Sink {
1263 fn write_str(&mut self, s: &str) -> fmt::Result {
1264 self.0.push_str(s);
1265 Ok(())
1266 }
1267 }
1268
1269 let mut engine = Engine::new(EscapeMode::Html);
1270 engine.add_template("t", "Hi {{ name }}").unwrap();
1271 let mut sink = Sink(String::new());
1272 engine.render_to("t", context! { name: "<b>" }, &mut sink).unwrap();
1273 assert_eq!(sink.0, "Hi <b>");
1274 }
1275
1276 #[test]
1279 fn test_error_position_is_template_absolute() {
1280 let mut engine = Engine::new(EscapeMode::Text);
1281 let err = engine
1283 .add_template("t", "aaaaaaaaaaaaaaaaaaaa{{ items[-1] }}")
1284 .unwrap_err();
1285 assert!(
1286 err.to_string().contains("1:30"),
1287 "expected template-absolute position 1:30, got: {err}"
1288 );
1289 }
1290
1291 #[test]
1292 fn test_error_position_on_later_line() {
1293 let mut engine = Engine::new(EscapeMode::Text);
1294 let src = "a\n{{ items[-1] }}";
1295 let err = engine.add_template("t", src).unwrap_err();
1296 assert!(err.to_string().contains("2:10"), "expected position 2:10, got: {err}");
1297 }
1298
1299 #[test]
1300 fn test_unknown_filter_position() {
1301 let mut engine = Engine::new(EscapeMode::Text);
1302 let err = engine.add_template("t", "prefix {{ x | nope }}").unwrap_err();
1303 assert!(err.to_string().contains("1:15"), "expected position 1:15, got: {err}");
1305 }
1306
1307 #[test]
1308 fn test_multiline_tag_error_position() {
1309 let mut engine = Engine::new(EscapeMode::Text);
1310 let err = engine
1312 .add_template("t", "{% if items[\n-1] %}x{% endif %}")
1313 .unwrap_err();
1314 assert!(err.to_string().contains("2:1"), "expected position 2:1, got: {err}");
1315 }
1316
1317 #[test]
1320 fn test_endblock_with_matching_label() {
1321 let mut engine = Engine::new(EscapeMode::Text);
1322 engine
1323 .add_template("t", "{% block content %}hi{% endblock content %}")
1324 .unwrap();
1325 assert_eq!(engine.render("t", context! {}).unwrap(), "hi");
1326 }
1327
1328 #[test]
1329 fn test_endblock_without_label() {
1330 let mut engine = Engine::new(EscapeMode::Text);
1331 engine.add_template("t", "{% block content %}hi{% endblock %}").unwrap();
1332 assert_eq!(engine.render("t", context! {}).unwrap(), "hi");
1333 }
1334
1335 #[test]
1336 fn test_endblock_with_mismatched_label_is_error() {
1337 let mut engine = Engine::new(EscapeMode::Text);
1338 let err = engine
1339 .add_template("t", "{% block content %}hi{% endblock footer %}")
1340 .unwrap_err();
1341 assert!(
1342 err.to_string().contains("endblock content"),
1343 "expected mismatch message, got: {err}"
1344 );
1345 }
1346
1347 #[test]
1348 fn test_endfor_label_still_rejected() {
1349 let mut engine = Engine::new(EscapeMode::Text);
1350 assert!(engine.add_template("t", "{% for i in x %}{% endfor i %}").is_err());
1351 }
1352
1353 #[test]
1354 fn test_non_ascii_identifier_rejected() {
1355 let mut engine = Engine::new(EscapeMode::Text);
1356 assert!(engine.add_template("t", "{{ café }}").is_err());
1357 }
1358}