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
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 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 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 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 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
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 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 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
493fn 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
506fn 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 Range<'_> {
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(ValueCow::Borrowed).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
630fn 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 {expected}, found `{actual}`"))
639}
640
641#[cfg(test)]
642mod test {
643 use liquid_core::model::ValueView;
644 use liquid_core::parser;
645 use liquid_core::runtime::RuntimeBuilder;
646 use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
647
648 use crate::stdlib;
649
650 use super::*;
651
652 fn options() -> Language {
653 let mut options = Language::default();
654 options.blocks.register("for".to_owned(), ForBlock.into());
655 options
656 .blocks
657 .register("tablerow".to_owned(), TableRowBlock.into());
658 options
659 .tags
660 .register("assign".to_owned(), stdlib::AssignTag.into());
661 options
662 }
663
664 #[test]
665 fn loop_over_array() {
666 let text = concat!("{% for name in array %}", "test {{name}} ", "{% endfor %}",);
667
668 let template = parser::parse(text, &options()).map(Template::new).unwrap();
669
670 let runtime = RuntimeBuilder::new().build();
671 runtime.set_global(
672 "array".into(),
673 Value::Array(vec![
674 Value::scalar(22f64),
675 Value::scalar(23f64),
676 Value::scalar(24f64),
677 Value::scalar("wat".to_owned()),
678 ]),
679 );
680 let output = template.render(&runtime).unwrap();
681 assert_eq!(output, "test 22 test 23 test 24 test wat ");
682 }
683
684 #[test]
685 fn loop_over_range_literals() {
686 let text = concat!(
687 "{% for name in (42..46) %}",
688 "#{{forloop.index}} test {{name}} | ",
689 "{% endfor %}",
690 );
691
692 let template = parser::parse(text, &options()).map(Template::new).unwrap();
693
694 let runtime = RuntimeBuilder::new().build();
695 let output = template.render(&runtime).unwrap();
696 assert_eq!(
697 output,
698 "#1 test 42 | #2 test 43 | #3 test 44 | #4 test 45 | #5 test 46 | "
699 );
700 }
701
702 #[test]
703 fn loop_over_range_vars() {
704 let text = concat!(
705 "{% for x in (alpha .. omega) %}",
706 "#{{forloop.index}} test {{x}}, ",
707 "{% endfor %}"
708 );
709 let template = parser::parse(text, &options()).map(Template::new).unwrap();
710
711 let runtime = RuntimeBuilder::new().build();
712 runtime.set_global("alpha".into(), Value::scalar(42i64));
713 runtime.set_global("omega".into(), Value::scalar(46i64));
714 let output = template.render(&runtime).unwrap();
715 assert_eq!(
716 output,
717 "#1 test 42, #2 test 43, #3 test 44, #4 test 45, #5 test 46, "
718 );
719 }
720
721 #[test]
722 fn nested_forloops() {
723 let text = concat!(
727 "{% for outer in (1..5) %}",
728 ">>{{forloop.index0}}:{{outer}}>>",
729 "{% for inner in (6..10) %}",
730 "{{outer}}:{{forloop.index0}}:{{inner}},",
731 "{% endfor %}",
732 ">>{{outer}}>>\n",
733 "{% endfor %}"
734 );
735 let template = parser::parse(text, &options()).map(Template::new).unwrap();
736
737 let runtime = RuntimeBuilder::new().build();
738 let output = template.render(&runtime).unwrap();
739 assert_eq!(
740 output,
741 concat!(
742 ">>0:1>>1:0:6,1:1:7,1:2:8,1:3:9,1:4:10,>>1>>\n",
743 ">>1:2>>2:0:6,2:1:7,2:2:8,2:3:9,2:4:10,>>2>>\n",
744 ">>2:3>>3:0:6,3:1:7,3:2:8,3:3:9,3:4:10,>>3>>\n",
745 ">>3:4>>4:0:6,4:1:7,4:2:8,4:3:9,4:4:10,>>4>>\n",
746 ">>4:5>>5:0:6,5:1:7,5:2:8,5:3:9,5:4:10,>>5>>\n",
747 )
748 );
749 }
750
751 #[test]
752 fn nested_forloops_with_else() {
753 let text = concat!(
755 "{% for x in i %}",
756 "{% for y in j %}inner{% else %}empty inner{% endfor %}",
757 "{% else %}",
758 "empty outer",
759 "{% endfor %}"
760 );
761 let template = parser::parse(text, &options()).map(Template::new).unwrap();
762
763 let runtime = RuntimeBuilder::new().build();
764 runtime.set_global("i".into(), Value::Array(vec![]));
765 runtime.set_global("j".into(), Value::Array(vec![]));
766 let output = template.render(&runtime).unwrap();
767 assert_eq!(output, "empty outer");
768
769 runtime.set_global("i".into(), Value::Array(vec![Value::scalar(1i64)]));
770 runtime.set_global("j".into(), Value::Array(vec![]));
771 let output = template.render(&runtime).unwrap();
772 assert_eq!(output, "empty inner");
773 }
774
775 #[test]
776 fn degenerate_range_is_safe() {
777 let text = concat!("{% for x in (10 .. 0) %}", "{{x}}", "{% endfor %}");
780 let template = parser::parse(text, &options()).map(Template::new).unwrap();
781
782 let runtime = RuntimeBuilder::new().build();
783 let output = template.render(&runtime).unwrap();
784 assert_eq!(output, "");
785 }
786
787 #[test]
788 fn limited_loop() {
789 let text = concat!(
790 "{% for i in (1..100) limit:2 %}",
791 "{{ i }} ",
792 "{% endfor %}"
793 );
794 let template = parser::parse(text, &options()).map(Template::new).unwrap();
795
796 let runtime = RuntimeBuilder::new().build();
797 let output = template.render(&runtime).unwrap();
798 assert_eq!(output, "1 2 ");
799 }
800
801 #[test]
802 fn offset_loop() {
803 let text = concat!(
804 "{% for i in (1..10) offset:4 %}",
805 "{{ i }} ",
806 "{% endfor %}"
807 );
808 let template = parser::parse(text, &options()).map(Template::new).unwrap();
809
810 let runtime = RuntimeBuilder::new().build();
811 let output = template.render(&runtime).unwrap();
812 assert_eq!(output, "5 6 7 8 9 10 ");
813 }
814
815 #[test]
816 fn offset_and_limited_loop() {
817 let text = concat!(
818 "{% for i in (1..10) offset:4 limit:2 %}",
819 "{{ i }} ",
820 "{% endfor %}"
821 );
822 let template = parser::parse(text, &options()).map(Template::new).unwrap();
823
824 let runtime = RuntimeBuilder::new().build();
825 let output = template.render(&runtime).unwrap();
826 assert_eq!(output, "5 6 ");
827 }
828
829 #[test]
830 fn reversed_loop() {
831 let text = concat!(
832 "{% for i in (1..10) reversed %}",
833 "{{ i }} ",
834 "{% endfor %}"
835 );
836 let template = parser::parse(text, &options()).map(Template::new).unwrap();
837
838 let runtime = RuntimeBuilder::new().build();
839 let output = template.render(&runtime).unwrap();
840 assert_eq!(output, "10 9 8 7 6 5 4 3 2 1 ");
841 }
842
843 #[test]
844 fn sliced_and_reversed_loop() {
845 let text = concat!(
846 "{% for i in (1..10) reversed offset:1 limit:5%}",
847 "{{ i }} ",
848 "{% endfor %}"
849 );
850 let template = parser::parse(text, &options()).map(Template::new).unwrap();
851
852 let runtime = RuntimeBuilder::new().build();
853 let output = template.render(&runtime).unwrap();
854 assert_eq!(output, "6 5 4 3 2 ");
855 }
856
857 #[test]
858 fn empty_loop_invokes_else_template() {
859 let text = concat!(
860 "{% for i in (1..10) limit:0 %}",
861 "{{ i }} ",
862 "{% else %}",
863 "There are no items!",
864 "{% endfor %}"
865 );
866 let template = parser::parse(text, &options()).map(Template::new).unwrap();
867
868 let runtime = RuntimeBuilder::new().build();
869 let output = template.render(&runtime).unwrap();
870 assert_eq!(output, "There are no items!");
871 }
872
873 #[test]
874 fn nil_loop_invokes_else_template() {
875 let text = concat!(
876 "{% for i in nil %}",
877 "{{ i }} ",
878 "{% else %}",
879 "There are no items!",
880 "{% endfor %}"
881 );
882 let template = parser::parse(text, &options()).map(Template::new).unwrap();
883
884 let runtime = RuntimeBuilder::new().build();
885 let output = template.render(&runtime).unwrap();
886 assert_eq!(output, "There are no items!");
887 }
888
889 #[test]
890 fn limit_greater_than_iterator_length() {
891 let text = concat!("{% for i in (1..5) limit:10 %}", "{{ i }} ", "{% endfor %}");
892 let template = parser::parse(text, &options()).map(Template::new).unwrap();
893
894 let runtime = RuntimeBuilder::new().build();
895 let output = template.render(&runtime).unwrap();
896 assert_eq!(output, "1 2 3 4 5 ");
897 }
898
899 #[test]
900 fn loop_variables() {
901 let text = concat!(
902 "{% for v in (100..102) %}",
903 "length: {{forloop.length}}, ",
904 "index: {{forloop.index}}, ",
905 "index0: {{forloop.index0}}, ",
906 "rindex: {{forloop.rindex}}, ",
907 "rindex0: {{forloop.rindex0}}, ",
908 "value: {{v}}, ",
909 "first: {{forloop.first}}, ",
910 "last: {{forloop.last}}\n",
911 "{% endfor %}",
912 );
913
914 let template = parser::parse(text, &options()).map(Template::new).unwrap();
915
916 let runtime = RuntimeBuilder::new().build();
917 let output = template.render(&runtime).unwrap();
918 assert_eq!(
919 output,
920 concat!(
921 "length: 3, index: 1, index0: 0, rindex: 3, rindex0: 2, value: 100, first: true, last: false\n",
922 "length: 3, index: 2, index0: 1, rindex: 2, rindex0: 1, value: 101, first: false, last: false\n",
923 "length: 3, index: 3, index0: 2, rindex: 1, rindex0: 0, value: 102, first: false, last: true\n",
924 )
925 );
926 }
927
928 #[derive(Clone, ParseFilter, FilterReflection)]
929 #[filter(name = "shout", description = "tests helper", parsed(ShoutFilter))]
930 pub(super) struct ShoutFilterParser;
931
932 #[derive(Debug, Default, Display_filter)]
933 #[name = "shout"]
934 pub(super) struct ShoutFilter;
935
936 impl Filter for ShoutFilter {
937 fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
938 Ok(Value::scalar(input.to_kstr().to_uppercase()))
939 }
940 }
941
942 #[test]
943 fn use_filters() {
944 let text = concat!(
945 "{% for name in array %}",
946 "test {{name | shout}} ",
947 "{% endfor %}",
948 );
949
950 let mut options = options();
951 options
952 .filters
953 .register("shout".to_owned(), Box::new(ShoutFilterParser));
954 let template = parser::parse(text, &options).map(Template::new).unwrap();
955
956 let runtime = RuntimeBuilder::new().build();
957
958 runtime.set_global(
959 "array".into(),
960 Value::Array(vec![
961 Value::scalar("alpha"),
962 Value::scalar("beta"),
963 Value::scalar("gamma"),
964 ]),
965 );
966 let output = template.render(&runtime).unwrap();
967 assert_eq!(output, "test ALPHA test BETA test GAMMA ");
968 }
969
970 #[test]
971 fn for_loop_parameters_with_variables() {
972 let text = concat!(
973 "{% assign l = 4 %}",
974 "{% assign o = 5 %}",
975 "{% for i in (1..100) limit:l offset:o %}",
976 "{{ i }} ",
977 "{% endfor %}"
978 );
979 let template = parser::parse(text, &options()).map(Template::new).unwrap();
980
981 let runtime = RuntimeBuilder::new().build();
982 let output = template.render(&runtime).unwrap();
983 assert_eq!(output, "6 7 8 9 ");
984 }
985
986 #[test]
987 fn tablerow_without_cols() {
988 let text = concat!(
989 "{% tablerow name in array %}",
990 "test {{name}} ",
991 "{% endtablerow %}",
992 );
993
994 let template = parser::parse(text, &options()).map(Template::new).unwrap();
995
996 let runtime = RuntimeBuilder::new().build();
997 runtime.set_global(
998 "array".into(),
999 Value::Array(vec![
1000 Value::scalar(22f64),
1001 Value::scalar(23f64),
1002 Value::scalar(24f64),
1003 Value::scalar("wat".to_owned()),
1004 ]),
1005 );
1006 let output = template.render(&runtime).unwrap();
1007 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>");
1008 }
1009
1010 #[test]
1011 fn tablerow_with_cols() {
1012 let text = concat!(
1013 "{% tablerow name in (42..46) cols:2 %}",
1014 "test {{name}} ",
1015 "{% endtablerow %}",
1016 );
1017
1018 let template = parser::parse(text, &options()).map(Template::new).unwrap();
1019
1020 let runtime = RuntimeBuilder::new().build();
1021 runtime.set_global(
1022 "array".into(),
1023 Value::Array(vec![
1024 Value::scalar(22f64),
1025 Value::scalar(23f64),
1026 Value::scalar(24f64),
1027 Value::scalar("wat".to_owned()),
1028 ]),
1029 );
1030 let output = template.render(&runtime).unwrap();
1031 assert_eq!(
1032 output,
1033 "<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>"
1034 );
1035 }
1036
1037 #[test]
1038 fn tablerow_loop_parameters_with_variables() {
1039 let text = concat!(
1040 "{% assign l = 4 %}",
1041 "{% assign o = 5 %}",
1042 "{% assign c = 3 %}",
1043 "{% tablerow i in (1..100) limit:l offset:o cols:c %}",
1044 "{{ i }} ",
1045 "{% endtablerow %}"
1046 );
1047 let template = parser::parse(text, &options()).map(Template::new).unwrap();
1048
1049 let runtime = RuntimeBuilder::new().build();
1050 let output = template.render(&runtime).unwrap();
1051 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>");
1052 }
1053
1054 #[test]
1055 fn tablerow_variables() {
1056 let text = concat!(
1057 "{% tablerow v in (100..103) cols:2 %}",
1058 "length: {{tablerow.length}}, ",
1059 "index: {{tablerow.index}}, ",
1060 "index0: {{tablerow.index0}}, ",
1061 "rindex: {{tablerow.rindex}}, ",
1062 "rindex0: {{tablerow.rindex0}}, ",
1063 "col: {{tablerow.col}}, ",
1064 "col0: {{tablerow.col0}}, ",
1065 "value: {{v}}, ",
1066 "first: {{tablerow.first}}, ",
1067 "last: {{tablerow.last}}, ",
1068 "col_first: {{tablerow.col_first}}, ",
1069 "col_last: {{tablerow.col_last}}",
1070 "{% endtablerow %}",
1071 );
1072
1073 let template = parser::parse(text, &options()).map(Template::new).unwrap();
1074
1075 let runtime = RuntimeBuilder::new().build();
1076 let output = template.render(&runtime).unwrap();
1077 assert_eq!(
1078 output,
1079 concat!(
1080 "<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>",
1081 "<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>",
1082 "<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>",
1083 "<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>",
1084 )
1085 );
1086 }
1087
1088 #[test]
1089 fn test_for_parentloop_nil_when_not_present() {
1090 let text = concat!(
1092 "{% for inner in outer %}",
1093 "{{ forloop.parentloop }}.{{ forloop.index }} ",
1095 "{% endfor %}"
1096 );
1097
1098 let template = parser::parse(text, &options()).map(Template::new).unwrap();
1099
1100 let runtime = RuntimeBuilder::new().build();
1101 runtime.set_global(
1102 "outer".into(),
1103 Value::Array(vec![
1104 Value::Array(vec![
1105 Value::scalar(1f64),
1106 Value::scalar(1f64),
1107 Value::scalar(1f64),
1108 ]),
1109 Value::Array(vec![
1110 Value::scalar(1f64),
1111 Value::scalar(1f64),
1112 Value::scalar(1f64),
1113 ]),
1114 ]),
1115 );
1116 let output = template.render(&runtime).unwrap();
1117 assert_eq!(output, ".1 .2 ");
1118 }
1119
1120 #[test]
1121 fn test_for_parentloop_references_parent_loop() {
1122 let text = concat!(
1123 "{% for inner in outer %}{% for k in inner %}",
1124 "{{ forloop.parentloop.index }}.{{ forloop.index }} ",
1125 "{% endfor %}{% endfor %}"
1126 );
1127
1128 let template = parser::parse(text, &options()).map(Template::new).unwrap();
1129
1130 let runtime = RuntimeBuilder::new().build();
1131 runtime.set_global(
1132 "outer".into(),
1133 Value::Array(vec![
1134 Value::Array(vec![
1135 Value::scalar(1f64),
1136 Value::scalar(1f64),
1137 Value::scalar(1f64),
1138 ]),
1139 Value::Array(vec![
1140 Value::scalar(1f64),
1141 Value::scalar(1f64),
1142 Value::scalar(1f64),
1143 ]),
1144 ]),
1145 );
1146 let output = template.render(&runtime).unwrap();
1147 assert_eq!(output, "1.1 1.2 1.3 2.1 2.2 2.3 ");
1148 }
1149}