1use std::fmt;
2use std::io::Write;
3
4use liquid_core::error::ResultLiquidExt;
5use liquid_core::model::{ValueView, ValueViewCmp};
6use liquid_core::parser::BlockElement;
7use liquid_core::parser::TagToken;
8use liquid_core::Expression;
9use liquid_core::Language;
10use liquid_core::Renderable;
11use liquid_core::Runtime;
12use liquid_core::Template;
13use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter};
14use liquid_core::{Error, Result};
15
16#[derive(Copy, Clone, Debug, Default)]
17pub struct IfBlock;
18
19impl IfBlock {
20 pub fn new() -> Self {
21 Self
22 }
23}
24
25impl BlockReflection for IfBlock {
26 fn start_tag(&self) -> &str {
27 "if"
28 }
29
30 fn end_tag(&self) -> &str {
31 "endif"
32 }
33
34 fn description(&self) -> &str {
35 ""
36 }
37}
38
39impl ParseBlock for IfBlock {
40 fn parse(
41 &self,
42 arguments: TagTokenIter<'_>,
43 mut tokens: TagBlock<'_, '_>,
44 options: &Language,
45 ) -> Result<Box<dyn Renderable>> {
46 let conditional = parse_if(arguments, &mut tokens, options)?;
47
48 tokens.assert_empty();
49 Ok(conditional)
50 }
51
52 fn reflection(&self) -> &dyn BlockReflection {
53 self
54 }
55}
56
57fn parse_if(
58 arguments: TagTokenIter<'_>,
59 tokens: &mut TagBlock<'_, '_>,
60 options: &Language,
61) -> Result<Box<dyn Renderable>> {
62 let condition = parse_condition(arguments)?;
63
64 let mut if_true = Vec::new();
65 let mut if_false = None;
66
67 while let Some(element) = tokens.next()? {
68 match element {
69 BlockElement::Tag(tag) => match tag.name() {
70 "else" => {
71 if_false = Some(tokens.parse_all(options)?);
72 break;
73 }
74 "elsif" => {
75 if_false = Some(vec![parse_if(tag.into_tokens(), tokens, options)?]);
76 break;
77 }
78 _ => if_true.push(tag.parse(tokens, options)?),
79 },
80 element => if_true.push(element.parse(tokens, options)?),
81 }
82 }
83
84 let if_true = Template::new(if_true);
85 let if_false = if_false.map(Template::new);
86
87 Ok(Box::new(Conditional {
88 condition,
89 mode: true,
90 if_true,
91 if_false,
92 }))
93}
94
95#[derive(Copy, Clone, Debug, Default)]
96pub struct UnlessBlock;
97
98impl UnlessBlock {
99 pub fn new() -> Self {
100 Self
101 }
102}
103
104impl BlockReflection for UnlessBlock {
105 fn start_tag(&self) -> &str {
106 "unless"
107 }
108
109 fn end_tag(&self) -> &str {
110 "endunless"
111 }
112
113 fn description(&self) -> &str {
114 ""
115 }
116}
117
118impl ParseBlock for UnlessBlock {
119 fn parse(
120 &self,
121 arguments: TagTokenIter<'_>,
122 mut tokens: TagBlock<'_, '_>,
123 options: &Language,
124 ) -> Result<Box<dyn Renderable>> {
125 let condition = parse_condition(arguments)?;
126
127 let mut if_true = Vec::new();
128 let mut if_false = None;
129
130 while let Some(element) = tokens.next()? {
131 match element {
132 BlockElement::Tag(tag) => match tag.name() {
133 "else" => {
134 if_false = Some(tokens.parse_all(options)?);
135 break;
136 }
137 _ => if_true.push(tag.parse(&mut tokens, options)?),
138 },
139 element => if_true.push(element.parse(&mut tokens, options)?),
140 }
141 }
142
143 let if_true = Template::new(if_true);
144 let if_false = if_false.map(Template::new);
145
146 tokens.assert_empty();
147 Ok(Box::new(Conditional {
148 condition,
149 mode: false,
150 if_true,
151 if_false,
152 }))
153 }
154
155 fn reflection(&self) -> &dyn BlockReflection {
156 self
157 }
158}
159
160#[derive(Debug)]
161struct Conditional {
162 condition: Condition,
163 mode: bool,
164 if_true: Template,
165 if_false: Option<Template>,
166}
167
168impl Conditional {
169 fn compare(&self, runtime: &dyn Runtime) -> Result<bool> {
170 let result = self.condition.evaluate(runtime)?;
171
172 Ok(result == self.mode)
173 }
174
175 fn trace(&self) -> String {
176 format!("{{% if {} %}}", self.condition)
177 }
178}
179
180impl Renderable for Conditional {
181 fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
182 let condition = self.compare(runtime).trace_with(|| self.trace().into())?;
183 if condition {
184 self.if_true
185 .render_to(writer, runtime)
186 .trace_with(|| self.trace().into())?;
187 } else if let Some(ref template) = self.if_false {
188 template
189 .render_to(writer, runtime)
190 .trace("{{% else %}}")
191 .trace_with(|| self.trace().into())?;
192 }
193
194 Ok(())
195 }
196}
197
198#[derive(Clone, Debug)]
199enum Condition {
200 Binary(BinaryCondition),
201 Existence(ExistenceCondition),
202 Conjunction(Box<Condition>, Box<Condition>),
203 Disjunction(Box<Condition>, Box<Condition>),
204}
205
206impl Condition {
207 pub(crate) fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
208 match *self {
209 Condition::Binary(ref c) => c.evaluate(runtime),
210 Condition::Existence(ref c) => c.evaluate(runtime),
211 Condition::Conjunction(ref left, ref right) => {
212 Ok(left.evaluate(runtime)? && right.evaluate(runtime)?)
213 }
214 Condition::Disjunction(ref left, ref right) => {
215 Ok(left.evaluate(runtime)? || right.evaluate(runtime)?)
216 }
217 }
218 }
219}
220
221impl fmt::Display for Condition {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 match *self {
224 Condition::Binary(ref c) => write!(f, "{c}"),
225 Condition::Existence(ref c) => write!(f, "{c}"),
226 Condition::Conjunction(ref left, ref right) => write!(f, "{left} and {right}"),
227 Condition::Disjunction(ref left, ref right) => write!(f, "{left} or {right}"),
228 }
229 }
230}
231
232#[derive(Clone, Debug)]
233struct BinaryCondition {
234 lh: Expression,
235 comparison: ComparisonOperator,
236 rh: Expression,
237}
238
239impl BinaryCondition {
240 pub(crate) fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
241 let a = self.lh.evaluate(runtime)?;
242 let ca = ValueViewCmp::new(a.as_view());
243 let b = self.rh.evaluate(runtime)?;
244 let cb = ValueViewCmp::new(b.as_view());
245
246 let result = match self.comparison {
247 ComparisonOperator::Equals => ca == cb,
248 ComparisonOperator::NotEquals => ca != cb,
249 ComparisonOperator::LessThan => ca < cb,
250 ComparisonOperator::GreaterThan => ca > cb,
251 ComparisonOperator::LessThanEquals => ca <= cb,
252 ComparisonOperator::GreaterThanEquals => ca >= cb,
253 ComparisonOperator::Contains => contains_check(a.as_view(), b.as_view())?,
254 };
255
256 Ok(result)
257 }
258}
259
260impl fmt::Display for BinaryCondition {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 write!(f, "{} {} {}", self.lh, self.comparison, self.rh)
263 }
264}
265
266fn contains_check(a: &dyn ValueView, b: &dyn ValueView) -> Result<bool> {
267 if let Some(a) = a.as_scalar() {
268 let b = b.to_kstr();
269 Ok(a.to_kstr().contains(b.as_str()))
270 } else if let Some(a) = a.as_object() {
271 let b = b.as_scalar();
272 let check = b
273 .map(|b| a.contains_key(b.to_kstr().as_str()))
274 .unwrap_or(false);
275 Ok(check)
276 } else if let Some(a) = a.as_array() {
277 for elem in a.values() {
278 if ValueViewCmp::new(elem) == ValueViewCmp::new(b) {
279 return Ok(true);
280 }
281 }
282 Ok(false)
283 } else {
284 Err(unexpected_value_error(
285 "string | array | object",
286 Some(a.type_name()),
287 ))
288 }
289}
290
291#[derive(Clone, Debug)]
292enum ComparisonOperator {
293 Equals,
294 NotEquals,
295 LessThan,
296 GreaterThan,
297 LessThanEquals,
298 GreaterThanEquals,
299 Contains,
300}
301
302impl fmt::Display for ComparisonOperator {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 let out = match *self {
305 ComparisonOperator::Equals => "==",
306 ComparisonOperator::NotEquals => "!=",
307 ComparisonOperator::LessThanEquals => "<=",
308 ComparisonOperator::GreaterThanEquals => ">=",
309 ComparisonOperator::LessThan => "<",
310 ComparisonOperator::GreaterThan => ">",
311 ComparisonOperator::Contains => "contains",
312 };
313 write!(f, "{out}")
314 }
315}
316
317impl ComparisonOperator {
318 fn from_str(s: &str) -> ::std::result::Result<Self, ()> {
319 match s {
320 "==" => Ok(ComparisonOperator::Equals),
321 "!=" | "<>" => Ok(ComparisonOperator::NotEquals),
322 "<" => Ok(ComparisonOperator::LessThan),
323 ">" => Ok(ComparisonOperator::GreaterThan),
324 "<=" => Ok(ComparisonOperator::LessThanEquals),
325 ">=" => Ok(ComparisonOperator::GreaterThanEquals),
326 "contains" => Ok(ComparisonOperator::Contains),
327 _ => Err(()),
328 }
329 }
330}
331
332#[derive(Clone, Debug)]
333struct ExistenceCondition {
334 lh: Expression,
335}
336
337impl ExistenceCondition {
338 pub(crate) fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
339 let a = self.lh.try_evaluate(runtime);
340 let a = a.unwrap_or_default();
341 let is_truthy = a.query_state(liquid_core::model::State::Truthy);
342 Ok(is_truthy)
343 }
344}
345
346impl fmt::Display for ExistenceCondition {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "{}", self.lh)
349 }
350}
351
352struct PeekableTagTokenIter<'a> {
353 iter: TagTokenIter<'a>,
354 peeked: Option<Option<TagToken<'a>>>,
355}
356
357impl<'a> Iterator for PeekableTagTokenIter<'a> {
358 type Item = TagToken<'a>;
359
360 fn next(&mut self) -> Option<TagToken<'a>> {
361 match self.peeked.take() {
362 Some(v) => v,
363 None => self.iter.next(),
364 }
365 }
366}
367
368impl<'a> PeekableTagTokenIter<'a> {
369 pub(crate) fn expect_next(&mut self, error_msg: &str) -> Result<TagToken<'a>> {
370 self.next().ok_or_else(|| self.iter.raise_error(error_msg))
371 }
372
373 fn peek(&mut self) -> Option<&TagToken<'a>> {
374 if self.peeked.is_none() {
375 self.peeked = Some(self.iter.next());
376 }
377 match self.peeked {
378 Some(Some(ref value)) => Some(value),
379 Some(None) => None,
380 None => unreachable!(),
381 }
382 }
383}
384
385fn parse_atom_condition(arguments: &mut PeekableTagTokenIter<'_>) -> Result<Condition> {
386 let lh = arguments
387 .expect_next("Value expected.")?
388 .expect_value()
389 .into_result()?;
390 let cond = match arguments
391 .peek()
392 .map(TagToken::as_str)
393 .and_then(|op| ComparisonOperator::from_str(op).ok())
394 {
395 Some(op) => {
396 arguments.next();
397 let rh = arguments
398 .expect_next("Value expected.")?
399 .expect_value()
400 .into_result()?;
401 Condition::Binary(BinaryCondition {
402 lh,
403 comparison: op,
404 rh,
405 })
406 }
407 None => Condition::Existence(ExistenceCondition { lh }),
408 };
409
410 Ok(cond)
411}
412
413fn parse_conjunction_chain(arguments: &mut PeekableTagTokenIter<'_>) -> Result<Condition> {
414 let mut lh = parse_atom_condition(arguments)?;
415
416 while let Some("and") = arguments.peek().map(TagToken::as_str) {
417 arguments.next();
418 let rh = parse_atom_condition(arguments)?;
419 lh = Condition::Conjunction(Box::new(lh), Box::new(rh));
420 }
421
422 Ok(lh)
423}
424
425fn parse_condition(arguments: TagTokenIter<'_>) -> Result<Condition> {
427 let mut arguments = PeekableTagTokenIter {
428 iter: arguments,
429 peeked: None,
430 };
431 let mut lh = parse_conjunction_chain(&mut arguments)?;
432
433 while let Some(token) = arguments.next() {
434 token
435 .expect_str("or")
436 .into_result_custom_msg("\"and\" or \"or\" expected.")?;
437
438 let rh = parse_conjunction_chain(&mut arguments)?;
439 lh = Condition::Disjunction(Box::new(lh), Box::new(rh));
440 }
441
442 Ok(lh)
443}
444
445fn unexpected_value_error<S: ToString>(expected: &str, actual: Option<S>) -> Error {
447 let actual = actual.map(|x| x.to_string());
448 unexpected_value_error_string(expected, actual)
449}
450
451fn unexpected_value_error_string(expected: &str, actual: Option<String>) -> Error {
452 let actual = actual.unwrap_or_else(|| "nothing".to_owned());
453 Error::with_msg(format!("Expected {expected}, found `{actual}`"))
454}
455
456#[cfg(test)]
457mod test {
458 use super::*;
459
460 use liquid_core::model::Object;
461 use liquid_core::model::Value;
462 use liquid_core::parser;
463 use liquid_core::runtime::RuntimeBuilder;
464
465 fn options() -> Language {
466 let mut options = Language::default();
467 options.blocks.register("if".to_owned(), IfBlock.into());
468 options
469 .blocks
470 .register("unless".to_owned(), UnlessBlock.into());
471 options
472 }
473
474 #[test]
475 fn number_comparison() {
476 let text = "{% if 6 < 7 %}if true{% endif %}";
477 let template = parser::parse(text, &options()).map(Template::new).unwrap();
478
479 let runtime = RuntimeBuilder::new().build();
480 let output = template.render(&runtime).unwrap();
481 assert_eq!(output, "if true");
482
483 let text = "{% if 7 < 6 %}if true{% else %}if false{% endif %}";
484 let template = parser::parse(text, &options()).map(Template::new).unwrap();
485
486 let runtime = RuntimeBuilder::new().build();
487 let output = template.render(&runtime).unwrap();
488 assert_eq!(output, "if false");
489 }
490
491 #[test]
492 fn string_comparison() {
493 let text = r#"{% if "one" == "one" %}if true{% endif %}"#;
494 let template = parser::parse(text, &options()).map(Template::new).unwrap();
495
496 let runtime = RuntimeBuilder::new().build();
497 let output = template.render(&runtime).unwrap();
498 assert_eq!(output, "if true");
499
500 let text = r#"{% if "one" == "two" %}if true{% else %}if false{% endif %}"#;
501 let template = parser::parse(text, &options()).map(Template::new).unwrap();
502
503 let runtime = RuntimeBuilder::new().build();
504 let output = template.render(&runtime).unwrap();
505 assert_eq!(output, "if false");
506 }
507
508 #[test]
509 fn implicit_comparison() {
510 let text = concat!(
511 "{% if truthy %}",
512 "yep",
513 "{% else %}",
514 "nope",
515 "{% endif %}"
516 );
517
518 let template = parser::parse(text, &options()).map(Template::new).unwrap();
519
520 let runtime = RuntimeBuilder::new().build();
522 let output = template.render(&runtime).unwrap();
523 assert_eq!(output, "nope");
524
525 let runtime = RuntimeBuilder::new().build();
527 runtime.set_global("truthy".into(), Value::Nil);
528 let output = template.render(&runtime).unwrap();
529 assert_eq!(output, "nope");
530
531 let runtime = RuntimeBuilder::new().build();
533 runtime.set_global("truthy".into(), Value::scalar(false));
534 let output = template.render(&runtime).unwrap();
535 assert_eq!(output, "nope");
536
537 let runtime = RuntimeBuilder::new().build();
539 runtime.set_global("truthy".into(), Value::scalar(true));
540 let output = template.render(&runtime).unwrap();
541 assert_eq!(output, "yep");
542 }
543
544 #[test]
545 fn unless() {
546 let text = concat!(
547 "{% unless some_value == 1 %}",
548 "unless body",
549 "{% endunless %}"
550 );
551
552 let template = parser::parse(text, &options()).map(Template::new).unwrap();
553
554 let runtime = RuntimeBuilder::new().build();
555 runtime.set_global("some_value".into(), Value::scalar(1f64));
556 let output = template.render(&runtime).unwrap();
557 assert_eq!(output, "");
558
559 let runtime = RuntimeBuilder::new().build();
560 runtime.set_global("some_value".into(), Value::scalar(42f64));
561 let output = template.render(&runtime).unwrap();
562 assert_eq!(output, "unless body");
563 }
564
565 #[test]
566 fn nested_if_else() {
567 let text = concat!(
568 "{% if truthy %}",
569 "yep, ",
570 "{% if also_truthy %}",
571 "also truthy",
572 "{% else %}",
573 "not also truthy",
574 "{% endif %}",
575 "{% else %}",
576 "nope",
577 "{% endif %}"
578 );
579 let template = parser::parse(text, &options()).map(Template::new).unwrap();
580
581 let runtime = RuntimeBuilder::new().build();
582 runtime.set_global("truthy".into(), Value::scalar(true));
583 runtime.set_global("also_truthy".into(), Value::scalar(false));
584 let output = template.render(&runtime).unwrap();
585 assert_eq!(output, "yep, not also truthy");
586 }
587
588 #[test]
589 fn multiple_elif_blocks() {
590 let text = concat!(
591 "{% if a == 1 %}",
592 "first",
593 "{% elsif a == 2 %}",
594 "second",
595 "{% elsif a == 3 %}",
596 "third",
597 "{% else %}",
598 "fourth",
599 "{% endif %}"
600 );
601
602 let template = parser::parse(text, &options()).map(Template::new).unwrap();
603
604 let runtime = RuntimeBuilder::new().build();
605 runtime.set_global("a".into(), Value::scalar(1f64));
606 let output = template.render(&runtime).unwrap();
607 assert_eq!(output, "first");
608
609 let runtime = RuntimeBuilder::new().build();
610 runtime.set_global("a".into(), Value::scalar(2f64));
611 let output = template.render(&runtime).unwrap();
612 assert_eq!(output, "second");
613
614 let runtime = RuntimeBuilder::new().build();
615 runtime.set_global("a".into(), Value::scalar(3f64));
616 let output = template.render(&runtime).unwrap();
617 assert_eq!(output, "third");
618
619 let runtime = RuntimeBuilder::new().build();
620 runtime.set_global("a".into(), Value::scalar("else"));
621 let output = template.render(&runtime).unwrap();
622 assert_eq!(output, "fourth");
623 }
624
625 #[test]
626 fn string_contains_with_literals() {
627 let text = "{% if \"Star Wars\" contains \"Star\" %}if true{% endif %}";
628 let template = parser::parse(text, &options()).map(Template::new).unwrap();
629
630 let runtime = RuntimeBuilder::new().build();
631 let output = template.render(&runtime).unwrap();
632 assert_eq!(output, "if true");
633
634 let text = "{% if \"Star Wars\" contains \"Alf\" %}if true{% else %}if false{% endif %}";
635 let template = parser::parse(text, &options()).map(Template::new).unwrap();
636
637 let runtime = RuntimeBuilder::new().build();
638 let output = template.render(&runtime).unwrap();
639 assert_eq!(output, "if false");
640 }
641
642 #[test]
643 fn string_contains_with_variables() {
644 let text = "{% if movie contains \"Star\" %}if true{% endif %}";
645 let template = parser::parse(text, &options()).map(Template::new).unwrap();
646
647 let runtime = RuntimeBuilder::new().build();
648 runtime.set_global("movie".into(), Value::scalar("Star Wars"));
649 let output = template.render(&runtime).unwrap();
650 assert_eq!(output, "if true");
651
652 let text = "{% if movie contains \"Star\" %}if true{% else %}if false{% endif %}";
653 let template = parser::parse(text, &options()).map(Template::new).unwrap();
654
655 let runtime = RuntimeBuilder::new().build();
656 runtime.set_global("movie".into(), Value::scalar("Batman"));
657 let output = template.render(&runtime).unwrap();
658 assert_eq!(output, "if false");
659 }
660
661 #[test]
662 fn contains_with_object_and_key() {
663 let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
664 let template = parser::parse(text, &options()).map(Template::new).unwrap();
665
666 let runtime = RuntimeBuilder::new().build();
667 let mut obj = Object::new();
668 obj.insert("Star Wars".into(), Value::scalar("1977"));
669 runtime.set_global("movies".into(), Value::Object(obj));
670 let output = template.render(&runtime).unwrap();
671 assert_eq!(output, "if true");
672 }
673
674 #[test]
675 fn contains_with_object_and_missing_key() {
676 let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
677 let template = parser::parse(text, &options()).map(Template::new).unwrap();
678
679 let runtime = RuntimeBuilder::new().build();
680 let obj = Object::new();
681 runtime.set_global("movies".into(), Value::Object(obj));
682 let output = template.render(&runtime).unwrap();
683 assert_eq!(output, "if false");
684 }
685
686 #[test]
687 fn contains_with_array_and_match() {
688 let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
689 let template = parser::parse(text, &options()).map(Template::new).unwrap();
690
691 let runtime = RuntimeBuilder::new().build();
692 let arr = vec![
693 Value::scalar("Star Wars"),
694 Value::scalar("Star Trek"),
695 Value::scalar("Alien"),
696 ];
697 runtime.set_global("movies".into(), Value::Array(arr));
698 let output = template.render(&runtime).unwrap();
699 assert_eq!(output, "if true");
700 }
701
702 #[test]
703 fn contains_with_array_and_no_match() {
704 let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
705 let template = parser::parse(text, &options()).map(Template::new).unwrap();
706
707 let runtime = RuntimeBuilder::new().build();
708 let arr = vec![Value::scalar("Alien")];
709 runtime.set_global("movies".into(), Value::Array(arr));
710 let output = template.render(&runtime).unwrap();
711 assert_eq!(output, "if false");
712 }
713
714 #[test]
715 fn multiple_conditions_and() {
716 let text = "{% if 1 == 1 and 2 == 2 %}if true{% else %}if false{% endif %}";
717 let template = parser::parse(text, &options()).map(Template::new).unwrap();
718
719 let runtime = RuntimeBuilder::new().build();
720 let output = template.render(&runtime).unwrap();
721 assert_eq!(output, "if true");
722
723 let text = "{% if 1 == 1 and 2 != 2 %}if true{% else %}if false{% endif %}";
724 let template = parser::parse(text, &options()).map(Template::new).unwrap();
725
726 let runtime = RuntimeBuilder::new().build();
727 let output = template.render(&runtime).unwrap();
728 assert_eq!(output, "if false");
729 }
730
731 #[test]
732 fn multiple_conditions_or() {
733 let text = "{% if 1 == 1 or 2 != 2 %}if true{% else %}if false{% endif %}";
734 let template = parser::parse(text, &options()).map(Template::new).unwrap();
735
736 let runtime = RuntimeBuilder::new().build();
737 let output = template.render(&runtime).unwrap();
738 assert_eq!(output, "if true");
739
740 let text = "{% if 1 != 1 or 2 != 2 %}if true{% else %}if false{% endif %}";
741 let template = parser::parse(text, &options()).map(Template::new).unwrap();
742
743 let runtime = RuntimeBuilder::new().build();
744 let output = template.render(&runtime).unwrap();
745 assert_eq!(output, "if false");
746 }
747
748 #[test]
749 fn multiple_conditions_and_or() {
750 let text = "{% if 1 == 1 or 2 == 2 and 3 != 3 %}if true{% else %}if false{% endif %}";
751 let template = parser::parse(text, &options()).map(Template::new).unwrap();
752
753 let runtime = RuntimeBuilder::new().build();
754 let output = template.render(&runtime).unwrap();
755 assert_eq!(output, "if true");
756 }
757}