liquid_lib/stdlib/blocks/
if_block.rs

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
425/// Common parsing for "if" and "unless" condition
426fn 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
445/// Format an error for an unexpected value.
446fn 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        // Non-existence
521        let runtime = RuntimeBuilder::new().build();
522        let output = template.render(&runtime).unwrap();
523        assert_eq!(output, "nope");
524
525        // Explicit nil
526        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        // false
532        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        // true
538        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}