loose_liquid_lib/stdlib/blocks/
for_block.rs

1use std::fmt;
2use std::io::Write;
3
4use liquid_core::error::{ResultLiquidExt, ResultLiquidReplaceExt};
5use liquid_core::model::{Object, ObjectView, Value, ValueCow, ValueView};
6use liquid_core::parser::BlockElement;
7use liquid_core::parser::TryMatchToken;
8use liquid_core::runtime::{Interrupt, InterruptRegister};
9use liquid_core::Expression;
10use liquid_core::Language;
11use liquid_core::Renderable;
12use liquid_core::Template;
13use liquid_core::{runtime::StackFrame, Runtime};
14use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter};
15use liquid_core::{Error, Result};
16
17#[derive(Copy, Clone, Debug, Default)]
18pub struct ForBlock;
19
20impl ForBlock {
21    pub fn new() -> Self {
22        Self::default()
23    }
24}
25
26impl BlockReflection for ForBlock {
27    fn start_tag(&self) -> &str {
28        "for"
29    }
30
31    fn end_tag(&self) -> &str {
32        "endfor"
33    }
34
35    fn description(&self) -> &str {
36        ""
37    }
38}
39
40impl ParseBlock for ForBlock {
41    fn parse(
42        &self,
43        mut arguments: TagTokenIter<'_>,
44        mut tokens: TagBlock<'_, '_>,
45        options: &Language,
46    ) -> Result<Box<dyn Renderable>> {
47        let var_name = arguments
48            .expect_next("Identifier expected.")?
49            .expect_identifier()
50            .into_result()?;
51
52        arguments
53            .expect_next("\"in\" expected.")?
54            .expect_str("in")
55            .into_result_custom_msg("\"in\" expected.")?;
56
57        let range = arguments.expect_next("Array or range expected.")?;
58        let range = match range.expect_value() {
59            TryMatchToken::Matches(array) => RangeExpression::Array(array),
60            TryMatchToken::Fails(range) => match range.expect_range() {
61                TryMatchToken::Matches((start, stop)) => RangeExpression::Counted(start, stop),
62                TryMatchToken::Fails(range) => return range.raise_error().into_err(),
63            },
64        };
65
66        // now we get to check for parameters...
67        let mut limit = None;
68        let mut offset = None;
69        let mut reversed = false;
70
71        while let Some(token) = arguments.next() {
72            match token.as_str() {
73                "limit" => limit = Some(parse_attr(&mut arguments)?),
74                "offset" => offset = Some(parse_attr(&mut arguments)?),
75                "reversed" => reversed = true,
76                _ => {
77                    return token
78                        .raise_custom_error("\"limit\", \"offset\" or \"reversed\" expected.")
79                        .into_err();
80                }
81            }
82        }
83
84        // no more arguments should be supplied, trying to supply them is an error
85        arguments.expect_nothing()?;
86
87        let mut item_template = Vec::new();
88        let mut else_template = None;
89
90        while let Some(element) = tokens.next()? {
91            match element {
92                BlockElement::Tag(mut tag) => match tag.name() {
93                    "else" => {
94                        // no more arguments should be supplied, trying to supply them is an error
95                        tag.tokens().expect_nothing()?;
96                        else_template = Some(tokens.parse_all(options)?);
97                        break;
98                    }
99                    _ => item_template.push(tag.parse(&mut tokens, options)?),
100                },
101                element => item_template.push(element.parse(&mut tokens, options)?),
102            }
103        }
104
105        let item_template = Template::new(item_template);
106        let else_template = else_template.map(Template::new);
107
108        tokens.assert_empty();
109        Ok(Box::new(For {
110            var_name: liquid_core::model::KString::from_ref(var_name),
111            range,
112            item_template,
113            else_template,
114            limit,
115            offset,
116            reversed,
117        }))
118    }
119
120    fn reflection(&self) -> &dyn BlockReflection {
121        self
122    }
123}
124
125#[derive(Debug)]
126struct For {
127    var_name: liquid_core::model::KString,
128    range: RangeExpression,
129    item_template: Template,
130    else_template: Option<Template>,
131    limit: Option<Expression>,
132    offset: Option<Expression>,
133    reversed: bool,
134}
135
136impl For {
137    fn trace(&self) -> String {
138        trace_for_tag(
139            self.var_name.as_str(),
140            &self.range,
141            &self.limit,
142            &self.offset,
143            self.reversed,
144        )
145    }
146}
147
148fn trace_for_tag(
149    var_name: &str,
150    range: &RangeExpression,
151    limit: &Option<Expression>,
152    offset: &Option<Expression>,
153    reversed: bool,
154) -> String {
155    let mut parameters = vec![];
156    if let Some(limit) = limit {
157        parameters.push(format!("limit:{}", limit));
158    }
159    if let Some(offset) = offset {
160        parameters.push(format!("offset:{}", offset));
161    }
162    if reversed {
163        parameters.push("reversed".to_owned());
164    }
165    format!(
166        "{{% for {} in {} {} %}}",
167        var_name,
168        range,
169        itertools::join(parameters.iter(), ", ")
170    )
171}
172
173impl Renderable for For {
174    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
175        let range = self
176            .range
177            .evaluate(runtime)
178            .trace_with(|| self.trace().into())?;
179        let array = range.evaluate()?;
180        let limit = evaluate_attr(&self.limit, runtime)?;
181        let offset = evaluate_attr(&self.offset, runtime)?.unwrap_or(0);
182        let array = iter_array(array, limit, offset, self.reversed);
183
184        match array.len() {
185            0 => {
186                if let Some(ref t) = self.else_template {
187                    t.render_to(writer, runtime)
188                        .trace("{{% else %}}")
189                        .trace_with(|| self.trace().into())?;
190                }
191            }
192
193            range_len => {
194                let parentloop = runtime.try_get(&[liquid_core::model::Scalar::new("forloop")]);
195                let parentloop_ref = parentloop.as_ref().map(|v| v.as_view());
196                for (i, v) in array.into_iter().enumerate() {
197                    let forloop = ForloopObject::new(i, range_len).parentloop(parentloop_ref);
198                    let mut root = std::collections::HashMap::<
199                        liquid_core::model::KStringRef<'_>,
200                        &dyn ValueView,
201                    >::new();
202                    root.insert("forloop".into(), &forloop);
203                    root.insert(self.var_name.as_ref(), &v);
204
205                    let scope = StackFrame::new(runtime, &root);
206                    self.item_template
207                        .render_to(writer, &scope)
208                        .trace_with(|| self.trace().into())
209                        .context_key("index")
210                        .value_with(|| format!("{}", i + 1).into())?;
211
212                    // given that we're at the end of the loop body
213                    // already, dealing with a `continue` signal is just
214                    // clearing the interrupt and carrying on as normal. A
215                    // `break` requires some special handling, though.
216                    let current_interrupt =
217                        scope.registers().get_mut::<InterruptRegister>().reset();
218                    if let Some(Interrupt::Break) = current_interrupt {
219                        break;
220                    }
221                }
222            }
223        }
224        Ok(())
225    }
226}
227
228#[derive(Debug, Clone, ValueView, ObjectView)]
229pub struct ForloopObject<'p> {
230    length: i64,
231    parentloop: Option<&'p dyn ValueView>,
232    index0: i64,
233    index: i64,
234    rindex0: i64,
235    rindex: i64,
236    first: bool,
237    last: bool,
238}
239
240impl<'p> ForloopObject<'p> {
241    pub fn new(i: usize, len: usize) -> Self {
242        let i = i as i64;
243        let len = len as i64;
244        let first = i == 0;
245        let last = i == (len - 1);
246        Self {
247            length: len,
248            parentloop: None,
249            index0: i,
250            index: i + 1,
251            rindex0: len - i - 1,
252            rindex: len - i,
253            first,
254            last,
255        }
256    }
257
258    fn parentloop(mut self, parentloop: Option<&'p dyn ValueView>) -> Self {
259        self.parentloop = parentloop;
260        self
261    }
262}
263
264#[derive(Copy, Clone, Debug, Default)]
265pub struct TableRowBlock;
266
267impl TableRowBlock {
268    pub fn new() -> Self {
269        Self::default()
270    }
271}
272
273impl BlockReflection for TableRowBlock {
274    fn start_tag(&self) -> &str {
275        "tablerow"
276    }
277
278    fn end_tag(&self) -> &str {
279        "endtablerow"
280    }
281
282    fn description(&self) -> &str {
283        ""
284    }
285}
286
287impl ParseBlock for TableRowBlock {
288    fn parse(
289        &self,
290        mut arguments: TagTokenIter<'_>,
291        mut tokens: TagBlock<'_, '_>,
292        options: &Language,
293    ) -> Result<Box<dyn Renderable>> {
294        let var_name = arguments
295            .expect_next("Identifier expected.")?
296            .expect_identifier()
297            .into_result()?;
298
299        arguments
300            .expect_next("\"in\" expected.")?
301            .expect_str("in")
302            .into_result_custom_msg("\"in\" expected.")?;
303
304        let range = arguments.expect_next("Array or range expected.")?;
305        let range = match range.expect_value() {
306            TryMatchToken::Matches(array) => RangeExpression::Array(array),
307            TryMatchToken::Fails(range) => match range.expect_range() {
308                TryMatchToken::Matches((start, stop)) => RangeExpression::Counted(start, stop),
309                TryMatchToken::Fails(range) => return range.raise_error().into_err(),
310            },
311        };
312
313        // now we get to check for parameters...
314        let mut cols = None;
315        let mut limit = None;
316        let mut offset = None;
317
318        while let Some(token) = arguments.next() {
319            match token.as_str() {
320                "cols" => cols = Some(parse_attr(&mut arguments)?),
321                "limit" => limit = Some(parse_attr(&mut arguments)?),
322                "offset" => offset = Some(parse_attr(&mut arguments)?),
323                _ => {
324                    return token
325                        .raise_custom_error("\"cols\", \"limit\" or \"offset\" expected.")
326                        .into_err();
327                }
328            }
329        }
330
331        // no more arguments should be supplied, trying to supply them is an error
332        arguments.expect_nothing()?;
333
334        let item_template = Template::new(tokens.parse_all(options)?);
335
336        tokens.assert_empty();
337        Ok(Box::new(TableRow {
338            var_name: liquid_core::model::KString::from_ref(var_name),
339            range,
340            item_template,
341            cols,
342            limit,
343            offset,
344        }))
345    }
346
347    fn reflection(&self) -> &dyn BlockReflection {
348        self
349    }
350}
351
352#[derive(Debug)]
353struct TableRow {
354    var_name: liquid_core::model::KString,
355    range: RangeExpression,
356    item_template: Template,
357    cols: Option<Expression>,
358    limit: Option<Expression>,
359    offset: Option<Expression>,
360}
361
362impl TableRow {
363    fn trace(&self) -> String {
364        trace_tablerow_tag(
365            self.var_name.as_str(),
366            &self.range,
367            &self.cols,
368            &self.limit,
369            &self.offset,
370        )
371    }
372}
373
374fn trace_tablerow_tag(
375    var_name: &str,
376    range: &RangeExpression,
377    cols: &Option<Expression>,
378    limit: &Option<Expression>,
379    offset: &Option<Expression>,
380) -> String {
381    let mut parameters = vec![];
382    if let Some(cols) = cols {
383        parameters.push(format!("cols:{}", cols));
384    }
385    if let Some(limit) = limit {
386        parameters.push(format!("limit:{}", limit));
387    }
388    if let Some(offset) = offset {
389        parameters.push(format!("offset:{}", offset));
390    }
391    format!(
392        "{{% for {} in {} {} %}}",
393        var_name,
394        range,
395        itertools::join(parameters.iter(), ", ")
396    )
397}
398
399impl Renderable for TableRow {
400    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
401        let range = self
402            .range
403            .evaluate(runtime)
404            .trace_with(|| self.trace().into())?;
405        let array = range.evaluate()?;
406        let cols = evaluate_attr(&self.cols, runtime)?;
407        let limit = evaluate_attr(&self.limit, runtime)?;
408        let offset = evaluate_attr(&self.offset, runtime)?.unwrap_or(0);
409        let array = iter_array(array, limit, offset, false);
410
411        let mut helper_vars = Object::new();
412
413        let range_len = array.len();
414        helper_vars.insert("length".into(), Value::scalar(range_len as i64));
415
416        for (i, v) in array.into_iter().enumerate() {
417            let cols = cols.unwrap_or(range_len);
418            let col_index = i % cols;
419            let row_index = i / cols;
420
421            let tablerow = TableRowObject::new(i, range_len, col_index, cols);
422            let mut root = std::collections::HashMap::<
423                liquid_core::model::KStringRef<'_>,
424                &dyn ValueView,
425            >::new();
426            root.insert("tablerow".into(), &tablerow);
427            root.insert(self.var_name.as_ref(), &v);
428
429            if tablerow.col_first {
430                write!(writer, "<tr class=\"row{}\">", row_index + 1)
431                    .replace("Failed to render")?;
432            }
433            write!(writer, "<td class=\"col{}\">", col_index + 1).replace("Failed to render")?;
434
435            let scope = StackFrame::new(runtime, &root);
436            self.item_template
437                .render_to(writer, &scope)
438                .trace_with(|| self.trace().into())
439                .context_key("index")
440                .value_with(|| format!("{}", i + 1).into())?;
441
442            write!(writer, "</td>").replace("Failed to render")?;
443            if tablerow.col_last {
444                write!(writer, "</tr>").replace("Failed to render")?;
445            }
446        }
447
448        Ok(())
449    }
450}
451
452#[derive(Debug, Clone, ValueView, ObjectView)]
453struct TableRowObject {
454    length: i64,
455    index0: i64,
456    index: i64,
457    rindex0: i64,
458    rindex: i64,
459    first: bool,
460    last: bool,
461    col0: i64,
462    col: i64,
463    col_first: bool,
464    col_last: bool,
465}
466
467impl TableRowObject {
468    fn new(i: usize, len: usize, col: usize, cols: usize) -> Self {
469        let i = i as i64;
470        let len = len as i64;
471        let col = col as i64;
472        let cols = cols as i64;
473        let first = i == 0;
474        let last = i == (len - 1);
475        let col_first = col == 0;
476        let col_last = col == (cols - 1) || last;
477        Self {
478            length: len,
479            index0: i,
480            index: i + 1,
481            rindex0: len - i - 1,
482            rindex: len - i,
483            first,
484            last,
485            col0: col,
486            col: (col + 1),
487            col_first,
488            col_last,
489        }
490    }
491}
492
493/// Extracts an integer value or an identifier from the token stream
494fn parse_attr(arguments: &mut TagTokenIter<'_>) -> Result<Expression> {
495    arguments
496        .expect_next("\":\" expected.")?
497        .expect_str(":")
498        .into_result_custom_msg("\":\" expected.")?;
499
500    arguments
501        .expect_next("Value expected.")?
502        .expect_value()
503        .into_result()
504}
505
506/// Evaluates an attribute, returning Ok(None) if input is also None.
507fn evaluate_attr(attr: &Option<Expression>, runtime: &dyn Runtime) -> Result<Option<usize>> {
508    match attr {
509        Some(attr) => {
510            let value = attr.evaluate(runtime)?;
511            let value = value
512                .as_scalar()
513                .and_then(|s| s.to_integer())
514                .ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))?
515                as usize;
516            Ok(Some(value))
517        }
518        None => Ok(None),
519    }
520}
521
522#[derive(Clone, Debug)]
523pub enum RangeExpression {
524    Array(Expression),
525    Counted(Expression, Expression),
526}
527
528impl RangeExpression {
529    pub fn evaluate<'r>(&'r self, runtime: &'r dyn Runtime) -> Result<Range<'r>> {
530        let range = match *self {
531            RangeExpression::Array(ref array_id) => {
532                let array = array_id.evaluate(runtime)?;
533                Range::Array(array)
534            }
535
536            RangeExpression::Counted(ref start_arg, ref stop_arg) => {
537                let start = int_argument(start_arg, runtime, "start")? as i64;
538                let stop = int_argument(stop_arg, runtime, "end")? as i64;
539                Range::Counted(start, stop)
540            }
541        };
542
543        Ok(range)
544    }
545}
546
547impl fmt::Display for RangeExpression {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        match *self {
550            RangeExpression::Array(ref arr) => write!(f, "{}", arr),
551            RangeExpression::Counted(ref start, ref end) => write!(f, "({}..{})", start, end),
552        }
553    }
554}
555
556#[derive(Clone, Debug)]
557pub enum Range<'r> {
558    Array(ValueCow<'r>),
559    Counted(i64, i64),
560}
561
562impl<'r> Range<'r> {
563    pub fn evaluate(&self) -> Result<Vec<ValueCow<'_>>> {
564        let range = match self {
565            Range::Array(array) => get_array(array.as_view())?,
566
567            Range::Counted(start, stop) => {
568                let range = (*start)..=(*stop);
569                range.map(|x| Value::scalar(x).into()).collect()
570            }
571        };
572
573        Ok(range)
574    }
575}
576
577fn get_array(array: &dyn ValueView) -> Result<Vec<ValueCow<'_>>> {
578    if let Some(x) = array.as_array() {
579        Ok(x.values().map(|v| ValueCow::Borrowed(v)).collect())
580    } else if let Some(x) = array.as_object() {
581        let x = x
582            .iter()
583            .map(|(k, v)| {
584                let k = k.into_owned();
585                let arr = vec![Value::scalar(k), v.to_value()];
586                Value::Array(arr).into()
587            })
588            .collect();
589        Ok(x)
590    } else if array.is_state() || array.is_nil() {
591        Ok(vec![])
592    } else {
593        Err(unexpected_value_error("array", Some(array.type_name())))
594    }
595}
596
597fn int_argument(arg: &Expression, runtime: &dyn Runtime, arg_name: &str) -> Result<isize> {
598    let value = arg.evaluate(runtime)?;
599
600    let value = value
601        .as_scalar()
602        .and_then(|v| v.to_integer())
603        .ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))
604        .context_key_with(|| arg_name.to_owned().into())
605        .value_with(|| value.to_kstr().into_owned())?;
606
607    Ok(value as isize)
608}
609
610fn iter_array(
611    mut range: Vec<ValueCow<'_>>,
612    limit: Option<usize>,
613    offset: usize,
614    reversed: bool,
615) -> Vec<ValueCow<'_>> {
616    let offset = ::std::cmp::min(offset, range.len());
617    let limit = limit
618        .map(|l| ::std::cmp::min(l, range.len()))
619        .unwrap_or_else(|| range.len() - offset);
620    range.drain(0..offset);
621    range.resize(limit, Value::Nil.into());
622
623    if reversed {
624        range.reverse();
625    };
626
627    range
628}
629
630/// Format an error for an unexpected value.
631fn unexpected_value_error<S: ToString>(expected: &str, actual: Option<S>) -> Error {
632    let actual = actual.map(|x| x.to_string());
633    unexpected_value_error_string(expected, actual)
634}
635
636fn unexpected_value_error_string(expected: &str, actual: Option<String>) -> Error {
637    let actual = actual.unwrap_or_else(|| "nothing".to_owned());
638    Error::with_msg(format!("Expected {}, found `{}`", expected, actual))
639}
640
641#[cfg(test)]
642mod test {
643    use liquid_core::model::ValueView;
644    use liquid_core::parser;
645    use liquid_core::runtime;
646    use liquid_core::runtime::RuntimeBuilder;
647    use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
648
649    use crate::stdlib;
650
651    use super::*;
652
653    fn options() -> Language {
654        let mut options = Language::default();
655        options.blocks.register("for".to_string(), ForBlock.into());
656        options
657            .blocks
658            .register("tablerow".to_string(), TableRowBlock.into());
659        options
660            .tags
661            .register("assign".to_string(), stdlib::AssignTag.into());
662        options
663    }
664
665    #[test]
666    fn loop_over_array() {
667        let text = concat!("{% for name in array %}", "test {{name}} ", "{% endfor %}",);
668
669        let template = parser::parse(text, &options())
670            .map(runtime::Template::new)
671            .unwrap();
672
673        let runtime = RuntimeBuilder::new().build();
674        runtime.set_global(
675            "array".into(),
676            Value::Array(vec![
677                Value::scalar(22f64),
678                Value::scalar(23f64),
679                Value::scalar(24f64),
680                Value::scalar("wat".to_owned()),
681            ]),
682        );
683        let output = template.render(&runtime).unwrap();
684        assert_eq!(output, "test 22 test 23 test 24 test wat ");
685    }
686
687    #[test]
688    fn loop_over_range_literals() {
689        let text = concat!(
690            "{% for name in (42..46) %}",
691            "#{{forloop.index}} test {{name}} | ",
692            "{% endfor %}",
693        );
694
695        let template = parser::parse(text, &options())
696            .map(runtime::Template::new)
697            .unwrap();
698
699        let runtime = RuntimeBuilder::new().build();
700        let output = template.render(&runtime).unwrap();
701        assert_eq!(
702            output,
703            "#1 test 42 | #2 test 43 | #3 test 44 | #4 test 45 | #5 test 46 | "
704        );
705    }
706
707    #[test]
708    fn loop_over_range_vars() {
709        let text = concat!(
710            "{% for x in (alpha .. omega) %}",
711            "#{{forloop.index}} test {{x}}, ",
712            "{% endfor %}"
713        );
714        let template = parser::parse(text, &options())
715            .map(runtime::Template::new)
716            .unwrap();
717
718        let runtime = RuntimeBuilder::new().build();
719        runtime.set_global("alpha".into(), Value::scalar(42i64));
720        runtime.set_global("omega".into(), Value::scalar(46i64));
721        let output = template.render(&runtime).unwrap();
722        assert_eq!(
723            output,
724            "#1 test 42, #2 test 43, #3 test 44, #4 test 45, #5 test 46, "
725        );
726    }
727
728    #[test]
729    fn nested_forloops() {
730        // test that nest nested for loops work, and that the
731        // variable scopes between the inner and outer variable
732        // scopes do not overlap.
733        let text = concat!(
734            "{% for outer in (1..5) %}",
735            ">>{{forloop.index0}}:{{outer}}>>",
736            "{% for inner in (6..10) %}",
737            "{{outer}}:{{forloop.index0}}:{{inner}},",
738            "{% endfor %}",
739            ">>{{outer}}>>\n",
740            "{% endfor %}"
741        );
742        let template = parser::parse(text, &options())
743            .map(runtime::Template::new)
744            .unwrap();
745
746        let runtime = RuntimeBuilder::new().build();
747        let output = template.render(&runtime).unwrap();
748        assert_eq!(
749            output,
750            concat!(
751                ">>0:1>>1:0:6,1:1:7,1:2:8,1:3:9,1:4:10,>>1>>\n",
752                ">>1:2>>2:0:6,2:1:7,2:2:8,2:3:9,2:4:10,>>2>>\n",
753                ">>2:3>>3:0:6,3:1:7,3:2:8,3:3:9,3:4:10,>>3>>\n",
754                ">>3:4>>4:0:6,4:1:7,4:2:8,4:3:9,4:4:10,>>4>>\n",
755                ">>4:5>>5:0:6,5:1:7,5:2:8,5:3:9,5:4:10,>>5>>\n",
756            )
757        );
758    }
759
760    #[test]
761    fn nested_forloops_with_else() {
762        // test that nested for loops parse their `else` blocks correctly
763        let text = concat!(
764            "{% for x in i %}",
765            "{% for y in j %}inner{% else %}empty inner{% endfor %}",
766            "{% else %}",
767            "empty outer",
768            "{% endfor %}"
769        );
770        let template = parser::parse(text, &options())
771            .map(runtime::Template::new)
772            .unwrap();
773
774        let runtime = RuntimeBuilder::new().build();
775        runtime.set_global("i".into(), Value::Array(vec![]));
776        runtime.set_global("j".into(), Value::Array(vec![]));
777        let output = template.render(&runtime).unwrap();
778        assert_eq!(output, "empty outer");
779
780        runtime.set_global("i".into(), Value::Array(vec![Value::scalar(1i64)]));
781        runtime.set_global("j".into(), Value::Array(vec![]));
782        let output = template.render(&runtime).unwrap();
783        assert_eq!(output, "empty inner");
784    }
785
786    #[test]
787    fn degenerate_range_is_safe() {
788        // make sure that a degenerate range (i.e. where max < min)
789        // doesn't result in an infinite loop
790        let text = concat!("{% for x in (10 .. 0) %}", "{{x}}", "{% endfor %}");
791        let template = parser::parse(text, &options())
792            .map(runtime::Template::new)
793            .unwrap();
794
795        let runtime = RuntimeBuilder::new().build();
796        let output = template.render(&runtime).unwrap();
797        assert_eq!(output, "");
798    }
799
800    #[test]
801    fn limited_loop() {
802        let text = concat!(
803            "{% for i in (1..100) limit:2 %}",
804            "{{ i }} ",
805            "{% endfor %}"
806        );
807        let template = parser::parse(text, &options())
808            .map(runtime::Template::new)
809            .unwrap();
810
811        let runtime = RuntimeBuilder::new().build();
812        let output = template.render(&runtime).unwrap();
813        assert_eq!(output, "1 2 ");
814    }
815
816    #[test]
817    fn offset_loop() {
818        let text = concat!(
819            "{% for i in (1..10) offset:4 %}",
820            "{{ i }} ",
821            "{% endfor %}"
822        );
823        let template = parser::parse(text, &options())
824            .map(runtime::Template::new)
825            .unwrap();
826
827        let runtime = RuntimeBuilder::new().build();
828        let output = template.render(&runtime).unwrap();
829        assert_eq!(output, "5 6 7 8 9 10 ");
830    }
831
832    #[test]
833    fn offset_and_limited_loop() {
834        let text = concat!(
835            "{% for i in (1..10) offset:4 limit:2 %}",
836            "{{ i }} ",
837            "{% endfor %}"
838        );
839        let template = parser::parse(text, &options())
840            .map(runtime::Template::new)
841            .unwrap();
842
843        let runtime = RuntimeBuilder::new().build();
844        let output = template.render(&runtime).unwrap();
845        assert_eq!(output, "5 6 ");
846    }
847
848    #[test]
849    fn reversed_loop() {
850        let text = concat!(
851            "{% for i in (1..10) reversed %}",
852            "{{ i }} ",
853            "{% endfor %}"
854        );
855        let template = parser::parse(text, &options())
856            .map(runtime::Template::new)
857            .unwrap();
858
859        let runtime = RuntimeBuilder::new().build();
860        let output = template.render(&runtime).unwrap();
861        assert_eq!(output, "10 9 8 7 6 5 4 3 2 1 ");
862    }
863
864    #[test]
865    fn sliced_and_reversed_loop() {
866        let text = concat!(
867            "{% for i in (1..10) reversed offset:1 limit:5%}",
868            "{{ i }} ",
869            "{% endfor %}"
870        );
871        let template = parser::parse(text, &options())
872            .map(runtime::Template::new)
873            .unwrap();
874
875        let runtime = RuntimeBuilder::new().build();
876        let output = template.render(&runtime).unwrap();
877        assert_eq!(output, "6 5 4 3 2 ");
878    }
879
880    #[test]
881    fn empty_loop_invokes_else_template() {
882        let text = concat!(
883            "{% for i in (1..10) limit:0 %}",
884            "{{ i }} ",
885            "{% else %}",
886            "There are no items!",
887            "{% endfor %}"
888        );
889        let template = parser::parse(text, &options())
890            .map(runtime::Template::new)
891            .unwrap();
892
893        let runtime = RuntimeBuilder::new().build();
894        let output = template.render(&runtime).unwrap();
895        assert_eq!(output, "There are no items!");
896    }
897
898    #[test]
899    fn nil_loop_invokes_else_template() {
900        let text = concat!(
901            "{% for i in nil %}",
902            "{{ i }} ",
903            "{% else %}",
904            "There are no items!",
905            "{% endfor %}"
906        );
907        let template = parser::parse(text, &options())
908            .map(runtime::Template::new)
909            .unwrap();
910
911        let runtime = RuntimeBuilder::new().build();
912        let output = template.render(&runtime).unwrap();
913        assert_eq!(output, "There are no items!");
914    }
915
916    #[test]
917    fn limit_greater_than_iterator_length() {
918        let text = concat!("{% for i in (1..5) limit:10 %}", "{{ i }} ", "{% endfor %}");
919        let template = parser::parse(text, &options())
920            .map(runtime::Template::new)
921            .unwrap();
922
923        let runtime = RuntimeBuilder::new().build();
924        let output = template.render(&runtime).unwrap();
925        assert_eq!(output, "1 2 3 4 5 ");
926    }
927
928    #[test]
929    fn loop_variables() {
930        let text = concat!(
931            "{% for v in (100..102) %}",
932            "length: {{forloop.length}}, ",
933            "index: {{forloop.index}}, ",
934            "index0: {{forloop.index0}}, ",
935            "rindex: {{forloop.rindex}}, ",
936            "rindex0: {{forloop.rindex0}}, ",
937            "value: {{v}}, ",
938            "first: {{forloop.first}}, ",
939            "last: {{forloop.last}}\n",
940            "{% endfor %}",
941        );
942
943        let template = parser::parse(text, &options())
944            .map(runtime::Template::new)
945            .unwrap();
946
947        let runtime = RuntimeBuilder::new().build();
948        let output = template.render(&runtime).unwrap();
949        assert_eq!(
950                output,
951                concat!(
952    "length: 3, index: 1, index0: 0, rindex: 3, rindex0: 2, value: 100, first: true, last: false\n",
953    "length: 3, index: 2, index0: 1, rindex: 2, rindex0: 1, value: 101, first: false, last: false\n",
954    "length: 3, index: 3, index0: 2, rindex: 1, rindex0: 0, value: 102, first: false, last: true\n",
955    )
956            );
957    }
958
959    #[derive(Clone, ParseFilter, FilterReflection)]
960    #[filter(name = "shout", description = "tests helper", parsed(ShoutFilter))]
961    pub struct ShoutFilterParser;
962
963    #[derive(Debug, Default, Display_filter)]
964    #[name = "shout"]
965    pub struct ShoutFilter;
966
967    impl Filter for ShoutFilter {
968        fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
969            Ok(Value::scalar(input.to_kstr().to_uppercase()))
970        }
971    }
972
973    #[test]
974    fn use_filters() {
975        let text = concat!(
976            "{% for name in array %}",
977            "test {{name | shout}} ",
978            "{% endfor %}",
979        );
980
981        let mut options = options();
982        options
983            .filters
984            .register("shout".to_string(), Box::new(ShoutFilterParser));
985        let template = parser::parse(text, &options)
986            .map(runtime::Template::new)
987            .unwrap();
988
989        let runtime = RuntimeBuilder::new().build();
990
991        runtime.set_global(
992            "array".into(),
993            Value::Array(vec![
994                Value::scalar("alpha"),
995                Value::scalar("beta"),
996                Value::scalar("gamma"),
997            ]),
998        );
999        let output = template.render(&runtime).unwrap();
1000        assert_eq!(output, "test ALPHA test BETA test GAMMA ");
1001    }
1002
1003    #[test]
1004    fn for_loop_parameters_with_variables() {
1005        let text = concat!(
1006            "{% assign l = 4 %}",
1007            "{% assign o = 5 %}",
1008            "{% for i in (1..100) limit:l offset:o %}",
1009            "{{ i }} ",
1010            "{% endfor %}"
1011        );
1012        let template = parser::parse(text, &options())
1013            .map(runtime::Template::new)
1014            .unwrap();
1015
1016        let runtime = RuntimeBuilder::new().build();
1017        let output = template.render(&runtime).unwrap();
1018        assert_eq!(output, "6 7 8 9 ");
1019    }
1020
1021    #[test]
1022    fn tablerow_without_cols() {
1023        let text = concat!(
1024            "{% tablerow name in array %}",
1025            "test {{name}} ",
1026            "{% endtablerow %}",
1027        );
1028
1029        let template = parser::parse(text, &options())
1030            .map(runtime::Template::new)
1031            .unwrap();
1032
1033        let runtime = RuntimeBuilder::new().build();
1034        runtime.set_global(
1035            "array".into(),
1036            Value::Array(vec![
1037                Value::scalar(22f64),
1038                Value::scalar(23f64),
1039                Value::scalar(24f64),
1040                Value::scalar("wat".to_owned()),
1041            ]),
1042        );
1043        let output = template.render(&runtime).unwrap();
1044        assert_eq!(output, "<tr class=\"row1\"><td class=\"col1\">test 22 </td><td class=\"col2\">test 23 </td><td class=\"col3\">test 24 </td><td class=\"col4\">test wat </td></tr>");
1045    }
1046
1047    #[test]
1048    fn tablerow_with_cols() {
1049        let text = concat!(
1050            "{% tablerow name in (42..46) cols:2 %}",
1051            "test {{name}} ",
1052            "{% endtablerow %}",
1053        );
1054
1055        let template = parser::parse(text, &options())
1056            .map(runtime::Template::new)
1057            .unwrap();
1058
1059        let runtime = RuntimeBuilder::new().build();
1060        runtime.set_global(
1061            "array".into(),
1062            Value::Array(vec![
1063                Value::scalar(22f64),
1064                Value::scalar(23f64),
1065                Value::scalar(24f64),
1066                Value::scalar("wat".to_owned()),
1067            ]),
1068        );
1069        let output = template.render(&runtime).unwrap();
1070        assert_eq!(
1071                output,
1072                "<tr class=\"row1\"><td class=\"col1\">test 42 </td><td class=\"col2\">test 43 </td></tr><tr class=\"row2\"><td class=\"col1\">test 44 </td><td class=\"col2\">test 45 </td></tr><tr class=\"row3\"><td class=\"col1\">test 46 </td></tr>"
1073            );
1074    }
1075
1076    #[test]
1077    fn tablerow_loop_parameters_with_variables() {
1078        let text = concat!(
1079            "{% assign l = 4 %}",
1080            "{% assign o = 5 %}",
1081            "{% assign c = 3 %}",
1082            "{% tablerow i in (1..100) limit:l offset:o cols:c %}",
1083            "{{ i }} ",
1084            "{% endtablerow %}"
1085        );
1086        let template = parser::parse(text, &options())
1087            .map(runtime::Template::new)
1088            .unwrap();
1089
1090        let runtime = RuntimeBuilder::new().build();
1091        let output = template.render(&runtime).unwrap();
1092        assert_eq!(output, "<tr class=\"row1\"><td class=\"col1\">6 </td><td class=\"col2\">7 </td><td class=\"col3\">8 </td></tr><tr class=\"row2\"><td class=\"col1\">9 </td></tr>");
1093    }
1094
1095    #[test]
1096    fn tablerow_variables() {
1097        let text = concat!(
1098            "{% tablerow v in (100..103) cols:2 %}",
1099            "length: {{tablerow.length}}, ",
1100            "index: {{tablerow.index}}, ",
1101            "index0: {{tablerow.index0}}, ",
1102            "rindex: {{tablerow.rindex}}, ",
1103            "rindex0: {{tablerow.rindex0}}, ",
1104            "col: {{tablerow.col}}, ",
1105            "col0: {{tablerow.col0}}, ",
1106            "value: {{v}}, ",
1107            "first: {{tablerow.first}}, ",
1108            "last: {{tablerow.last}}, ",
1109            "col_first: {{tablerow.col_first}}, ",
1110            "col_last: {{tablerow.col_last}}",
1111            "{% endtablerow %}",
1112        );
1113
1114        let template = parser::parse(text, &options())
1115            .map(runtime::Template::new)
1116            .unwrap();
1117
1118        let runtime = RuntimeBuilder::new().build();
1119        let output = template.render(&runtime).unwrap();
1120        assert_eq!(
1121                output,
1122                concat!(
1123    "<tr class=\"row1\"><td class=\"col1\">length: 4, index: 1, index0: 0, rindex: 4, rindex0: 3, col: 1, col0: 0, value: 100, first: true, last: false, col_first: true, col_last: false</td>",
1124    "<td class=\"col2\">length: 4, index: 2, index0: 1, rindex: 3, rindex0: 2, col: 2, col0: 1, value: 101, first: false, last: false, col_first: false, col_last: true</td></tr>",
1125    "<tr class=\"row2\"><td class=\"col1\">length: 4, index: 3, index0: 2, rindex: 2, rindex0: 1, col: 1, col0: 0, value: 102, first: false, last: false, col_first: true, col_last: false</td>",
1126    "<td class=\"col2\">length: 4, index: 4, index0: 3, rindex: 1, rindex0: 0, col: 2, col0: 1, value: 103, first: false, last: true, col_first: false, col_last: true</td></tr>",
1127    )
1128            );
1129    }
1130
1131    #[test]
1132    fn test_for_parentloop_nil_when_not_present() {
1133        //NOTE: this test differs slightly from the liquid conformity test
1134        let text = concat!(
1135            "{% for inner in outer %}",
1136            // the liquid test has `forloop.parentloop.index` here
1137            "{{ forloop.parentloop }}.{{ forloop.index }} ",
1138            "{% endfor %}"
1139        );
1140
1141        let template = parser::parse(text, &options())
1142            .map(runtime::Template::new)
1143            .unwrap();
1144
1145        let runtime = RuntimeBuilder::new().build();
1146        runtime.set_global(
1147            "outer".into(),
1148            Value::Array(vec![
1149                Value::Array(vec![
1150                    Value::scalar(1f64),
1151                    Value::scalar(1f64),
1152                    Value::scalar(1f64),
1153                ]),
1154                Value::Array(vec![
1155                    Value::scalar(1f64),
1156                    Value::scalar(1f64),
1157                    Value::scalar(1f64),
1158                ]),
1159            ]),
1160        );
1161        let output = template.render(&runtime).unwrap();
1162        assert_eq!(output, ".1 .2 ");
1163    }
1164
1165    #[test]
1166    fn test_for_parentloop_references_parent_loop() {
1167        let text = concat!(
1168            "{% for inner in outer %}{% for k in inner %}",
1169            "{{ forloop.parentloop.index }}.{{ forloop.index }} ",
1170            "{% endfor %}{% endfor %}"
1171        );
1172
1173        let template = parser::parse(text, &options())
1174            .map(runtime::Template::new)
1175            .unwrap();
1176
1177        let runtime = RuntimeBuilder::new().build();
1178        runtime.set_global(
1179            "outer".into(),
1180            Value::Array(vec![
1181                Value::Array(vec![
1182                    Value::scalar(1f64),
1183                    Value::scalar(1f64),
1184                    Value::scalar(1f64),
1185                ]),
1186                Value::Array(vec![
1187                    Value::scalar(1f64),
1188                    Value::scalar(1f64),
1189                    Value::scalar(1f64),
1190                ]),
1191            ]),
1192        );
1193        let output = template.render(&runtime).unwrap();
1194        assert_eq!(output, "1.1 1.2 1.3 2.1 2.2 2.3 ");
1195    }
1196}