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 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::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 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<'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
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 {}, 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 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 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 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 let text = concat!(
1135 "{% for inner in outer %}",
1136 "{{ 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}