use std::fmt;
use std::io::Write;
use liquid_core::error::{ResultLiquidExt, ResultLiquidReplaceExt};
use liquid_core::model::{Object, ObjectView, Value, ValueCow, ValueView};
use liquid_core::parser::BlockElement;
use liquid_core::parser::TryMatchToken;
use liquid_core::runtime::{Interrupt, InterruptRegister};
use liquid_core::Expression;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Template;
use liquid_core::{runtime::StackFrame, Runtime};
use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter};
use liquid_core::{Error, Result};
#[derive(Copy, Clone, Debug, Default)]
pub struct ForBlock;
impl ForBlock {
pub fn new() -> Self {
Self::default()
}
}
impl BlockReflection for ForBlock {
fn start_tag(&self) -> &str {
"for"
}
fn end_tag(&self) -> &str {
"endfor"
}
fn description(&self) -> &str {
""
}
}
impl ParseBlock for ForBlock {
fn parse(
&self,
mut arguments: TagTokenIter<'_>,
mut tokens: TagBlock<'_, '_>,
options: &Language,
) -> Result<Box<dyn Renderable>> {
let var_name = arguments
.expect_next("Identifier expected.")?
.expect_identifier()
.into_result()?;
arguments
.expect_next("\"in\" expected.")?
.expect_str("in")
.into_result_custom_msg("\"in\" expected.")?;
let range = arguments.expect_next("Array or range expected.")?;
let range = match range.expect_value() {
TryMatchToken::Matches(array) => RangeExpression::Array(array),
TryMatchToken::Fails(range) => match range.expect_range() {
TryMatchToken::Matches((start, stop)) => RangeExpression::Counted(start, stop),
TryMatchToken::Fails(range) => return range.raise_error().into_err(),
},
};
let mut limit = None;
let mut offset = None;
let mut reversed = false;
while let Some(token) = arguments.next() {
match token.as_str() {
"limit" => limit = Some(parse_attr(&mut arguments)?),
"offset" => offset = Some(parse_attr(&mut arguments)?),
"reversed" => reversed = true,
_ => {
return token
.raise_custom_error("\"limit\", \"offset\" or \"reversed\" expected.")
.into_err();
}
}
}
arguments.expect_nothing()?;
let mut item_template = Vec::new();
let mut else_template = None;
while let Some(element) = tokens.next()? {
match element {
BlockElement::Tag(mut tag) => match tag.name() {
"else" => {
tag.tokens().expect_nothing()?;
else_template = Some(tokens.parse_all(options)?);
break;
}
_ => item_template.push(tag.parse(&mut tokens, options)?),
},
element => item_template.push(element.parse(&mut tokens, options)?),
}
}
let item_template = Template::new(item_template);
let else_template = else_template.map(Template::new);
tokens.assert_empty();
Ok(Box::new(For {
var_name: liquid_core::model::KString::from_ref(var_name),
range,
item_template,
else_template,
limit,
offset,
reversed,
}))
}
fn reflection(&self) -> &dyn BlockReflection {
self
}
}
#[derive(Debug)]
struct For {
var_name: liquid_core::model::KString,
range: RangeExpression,
item_template: Template,
else_template: Option<Template>,
limit: Option<Expression>,
offset: Option<Expression>,
reversed: bool,
}
impl For {
fn trace(&self) -> String {
trace_for_tag(
self.var_name.as_str(),
&self.range,
&self.limit,
&self.offset,
self.reversed,
)
}
}
fn trace_for_tag(
var_name: &str,
range: &RangeExpression,
limit: &Option<Expression>,
offset: &Option<Expression>,
reversed: bool,
) -> String {
let mut parameters = vec![];
if let Some(limit) = limit {
parameters.push(format!("limit:{}", limit));
}
if let Some(offset) = offset {
parameters.push(format!("offset:{}", offset));
}
if reversed {
parameters.push("reversed".to_owned());
}
format!(
"{{% for {} in {} {} %}}",
var_name,
range,
itertools::join(parameters.iter(), ", ")
)
}
impl Renderable for For {
fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
let range = self
.range
.evaluate(runtime)
.trace_with(|| self.trace().into())?;
let array = range.evaluate()?;
let limit = evaluate_attr(&self.limit, runtime)?;
let offset = evaluate_attr(&self.offset, runtime)?.unwrap_or(0);
let array = iter_array(array, limit, offset, self.reversed);
match array.len() {
0 => {
if let Some(ref t) = self.else_template {
t.render_to(writer, runtime)
.trace("{{% else %}}")
.trace_with(|| self.trace().into())?;
}
}
range_len => {
let parentloop = runtime.try_get(&[liquid_core::model::Scalar::new("forloop")]);
let parentloop_ref = parentloop.as_ref().map(|v| v.as_view());
for (i, v) in array.into_iter().enumerate() {
let forloop = ForloopObject::new(i, range_len).parentloop(parentloop_ref);
let mut root = std::collections::HashMap::<
liquid_core::model::KStringRef<'_>,
&dyn ValueView,
>::new();
root.insert("forloop".into(), &forloop);
root.insert(self.var_name.as_ref(), &v);
let scope = StackFrame::new(runtime, &root);
self.item_template
.render_to(writer, &scope)
.trace_with(|| self.trace().into())
.context_key("index")
.value_with(|| format!("{}", i + 1).into())?;
let current_interrupt =
scope.registers().get_mut::<InterruptRegister>().reset();
if let Some(Interrupt::Break) = current_interrupt {
break;
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, ValueView, ObjectView)]
pub struct ForloopObject<'p> {
length: i64,
parentloop: Option<&'p dyn ValueView>,
index0: i64,
index: i64,
rindex0: i64,
rindex: i64,
first: bool,
last: bool,
}
impl<'p> ForloopObject<'p> {
pub fn new(i: usize, len: usize) -> Self {
let i = i as i64;
let len = len as i64;
let first = i == 0;
let last = i == (len - 1);
Self {
length: len,
parentloop: None,
index0: i,
index: i + 1,
rindex0: len - i - 1,
rindex: len - i,
first,
last,
}
}
fn parentloop(mut self, parentloop: Option<&'p dyn ValueView>) -> Self {
self.parentloop = parentloop;
self
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct TableRowBlock;
impl TableRowBlock {
pub fn new() -> Self {
Self::default()
}
}
impl BlockReflection for TableRowBlock {
fn start_tag(&self) -> &str {
"tablerow"
}
fn end_tag(&self) -> &str {
"endtablerow"
}
fn description(&self) -> &str {
""
}
}
impl ParseBlock for TableRowBlock {
fn parse(
&self,
mut arguments: TagTokenIter<'_>,
mut tokens: TagBlock<'_, '_>,
options: &Language,
) -> Result<Box<dyn Renderable>> {
let var_name = arguments
.expect_next("Identifier expected.")?
.expect_identifier()
.into_result()?;
arguments
.expect_next("\"in\" expected.")?
.expect_str("in")
.into_result_custom_msg("\"in\" expected.")?;
let range = arguments.expect_next("Array or range expected.")?;
let range = match range.expect_value() {
TryMatchToken::Matches(array) => RangeExpression::Array(array),
TryMatchToken::Fails(range) => match range.expect_range() {
TryMatchToken::Matches((start, stop)) => RangeExpression::Counted(start, stop),
TryMatchToken::Fails(range) => return range.raise_error().into_err(),
},
};
let mut cols = None;
let mut limit = None;
let mut offset = None;
while let Some(token) = arguments.next() {
match token.as_str() {
"cols" => cols = Some(parse_attr(&mut arguments)?),
"limit" => limit = Some(parse_attr(&mut arguments)?),
"offset" => offset = Some(parse_attr(&mut arguments)?),
_ => {
return token
.raise_custom_error("\"cols\", \"limit\" or \"offset\" expected.")
.into_err();
}
}
}
arguments.expect_nothing()?;
let item_template = Template::new(tokens.parse_all(options)?);
tokens.assert_empty();
Ok(Box::new(TableRow {
var_name: liquid_core::model::KString::from_ref(var_name),
range,
item_template,
cols,
limit,
offset,
}))
}
fn reflection(&self) -> &dyn BlockReflection {
self
}
}
#[derive(Debug)]
struct TableRow {
var_name: liquid_core::model::KString,
range: RangeExpression,
item_template: Template,
cols: Option<Expression>,
limit: Option<Expression>,
offset: Option<Expression>,
}
impl TableRow {
fn trace(&self) -> String {
trace_tablerow_tag(
self.var_name.as_str(),
&self.range,
&self.cols,
&self.limit,
&self.offset,
)
}
}
fn trace_tablerow_tag(
var_name: &str,
range: &RangeExpression,
cols: &Option<Expression>,
limit: &Option<Expression>,
offset: &Option<Expression>,
) -> String {
let mut parameters = vec![];
if let Some(cols) = cols {
parameters.push(format!("cols:{}", cols));
}
if let Some(limit) = limit {
parameters.push(format!("limit:{}", limit));
}
if let Some(offset) = offset {
parameters.push(format!("offset:{}", offset));
}
format!(
"{{% for {} in {} {} %}}",
var_name,
range,
itertools::join(parameters.iter(), ", ")
)
}
impl Renderable for TableRow {
fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
let range = self
.range
.evaluate(runtime)
.trace_with(|| self.trace().into())?;
let array = range.evaluate()?;
let cols = evaluate_attr(&self.cols, runtime)?;
let limit = evaluate_attr(&self.limit, runtime)?;
let offset = evaluate_attr(&self.offset, runtime)?.unwrap_or(0);
let array = iter_array(array, limit, offset, false);
let mut helper_vars = Object::new();
let range_len = array.len();
helper_vars.insert("length".into(), Value::scalar(range_len as i64));
for (i, v) in array.into_iter().enumerate() {
let cols = cols.unwrap_or(range_len);
let col_index = i % cols;
let row_index = i / cols;
let tablerow = TableRowObject::new(i, range_len, col_index, cols);
let mut root = std::collections::HashMap::<
liquid_core::model::KStringRef<'_>,
&dyn ValueView,
>::new();
root.insert("tablerow".into(), &tablerow);
root.insert(self.var_name.as_ref(), &v);
if tablerow.col_first {
write!(writer, "<tr class=\"row{}\">", row_index + 1)
.replace("Failed to render")?;
}
write!(writer, "<td class=\"col{}\">", col_index + 1).replace("Failed to render")?;
let scope = StackFrame::new(runtime, &root);
self.item_template
.render_to(writer, &scope)
.trace_with(|| self.trace().into())
.context_key("index")
.value_with(|| format!("{}", i + 1).into())?;
write!(writer, "</td>").replace("Failed to render")?;
if tablerow.col_last {
write!(writer, "</tr>").replace("Failed to render")?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, ValueView, ObjectView)]
struct TableRowObject {
length: i64,
index0: i64,
index: i64,
rindex0: i64,
rindex: i64,
first: bool,
last: bool,
col0: i64,
col: i64,
col_first: bool,
col_last: bool,
}
impl TableRowObject {
fn new(i: usize, len: usize, col: usize, cols: usize) -> Self {
let i = i as i64;
let len = len as i64;
let col = col as i64;
let cols = cols as i64;
let first = i == 0;
let last = i == (len - 1);
let col_first = col == 0;
let col_last = col == (cols - 1) || last;
Self {
length: len,
index0: i,
index: i + 1,
rindex0: len - i - 1,
rindex: len - i,
first,
last,
col0: col,
col: (col + 1),
col_first,
col_last,
}
}
}
fn parse_attr(arguments: &mut TagTokenIter<'_>) -> Result<Expression> {
arguments
.expect_next("\":\" expected.")?
.expect_str(":")
.into_result_custom_msg("\":\" expected.")?;
arguments
.expect_next("Value expected.")?
.expect_value()
.into_result()
}
fn evaluate_attr(attr: &Option<Expression>, runtime: &dyn Runtime) -> Result<Option<usize>> {
match attr {
Some(attr) => {
let value = attr.evaluate(runtime)?;
let value = value
.as_scalar()
.and_then(|s| s.to_integer())
.ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))?
as usize;
Ok(Some(value))
}
None => Ok(None),
}
}
#[derive(Clone, Debug)]
pub enum RangeExpression {
Array(Expression),
Counted(Expression, Expression),
}
impl RangeExpression {
pub fn evaluate<'r>(&'r self, runtime: &'r dyn Runtime) -> Result<Range<'r>> {
let range = match *self {
RangeExpression::Array(ref array_id) => {
let array = array_id.evaluate(runtime)?;
Range::Array(array)
}
RangeExpression::Counted(ref start_arg, ref stop_arg) => {
let start = int_argument(start_arg, runtime, "start")? as i64;
let stop = int_argument(stop_arg, runtime, "end")? as i64;
Range::Counted(start, stop)
}
};
Ok(range)
}
}
impl fmt::Display for RangeExpression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
RangeExpression::Array(ref arr) => write!(f, "{}", arr),
RangeExpression::Counted(ref start, ref end) => write!(f, "({}..{})", start, end),
}
}
}
#[derive(Clone, Debug)]
pub enum Range<'r> {
Array(ValueCow<'r>),
Counted(i64, i64),
}
impl<'r> Range<'r> {
pub fn evaluate(&self) -> Result<Vec<ValueCow<'_>>> {
let range = match self {
Range::Array(array) => get_array(array.as_view())?,
Range::Counted(start, stop) => {
let range = (*start)..=(*stop);
range.map(|x| Value::scalar(x).into()).collect()
}
};
Ok(range)
}
}
fn get_array(array: &dyn ValueView) -> Result<Vec<ValueCow<'_>>> {
if let Some(x) = array.as_array() {
Ok(x.values().map(|v| ValueCow::Borrowed(v)).collect())
} else if let Some(x) = array.as_object() {
let x = x
.iter()
.map(|(k, v)| {
let k = k.into_owned();
let arr = vec![Value::scalar(k), v.to_value()];
Value::Array(arr).into()
})
.collect();
Ok(x)
} else if array.is_state() || array.is_nil() {
Ok(vec![])
} else {
Err(unexpected_value_error("array", Some(array.type_name())))
}
}
fn int_argument(arg: &Expression, runtime: &dyn Runtime, arg_name: &str) -> Result<isize> {
let value = arg.evaluate(runtime)?;
let value = value
.as_scalar()
.and_then(|v| v.to_integer())
.ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))
.context_key_with(|| arg_name.to_owned().into())
.value_with(|| value.to_kstr().into_owned())?;
Ok(value as isize)
}
fn iter_array(
mut range: Vec<ValueCow<'_>>,
limit: Option<usize>,
offset: usize,
reversed: bool,
) -> Vec<ValueCow<'_>> {
let offset = ::std::cmp::min(offset, range.len());
let limit = limit
.map(|l| ::std::cmp::min(l, range.len()))
.unwrap_or_else(|| range.len() - offset);
range.drain(0..offset);
range.resize(limit, Value::Nil.into());
if reversed {
range.reverse();
};
range
}
fn unexpected_value_error<S: ToString>(expected: &str, actual: Option<S>) -> Error {
let actual = actual.map(|x| x.to_string());
unexpected_value_error_string(expected, actual)
}
fn unexpected_value_error_string(expected: &str, actual: Option<String>) -> Error {
let actual = actual.unwrap_or_else(|| "nothing".to_owned());
Error::with_msg(format!("Expected {}, found `{}`", expected, actual))
}
#[cfg(test)]
mod test {
use liquid_core::model::ValueView;
use liquid_core::parser;
use liquid_core::runtime;
use liquid_core::runtime::RuntimeBuilder;
use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
use crate::stdlib;
use super::*;
fn options() -> Language {
let mut options = Language::default();
options.blocks.register("for".to_string(), ForBlock.into());
options
.blocks
.register("tablerow".to_string(), TableRowBlock.into());
options
.tags
.register("assign".to_string(), stdlib::AssignTag.into());
options
}
#[test]
fn loop_over_array() {
let text = concat!("{% for name in array %}", "test {{name}} ", "{% endfor %}",);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"array".into(),
Value::Array(vec![
Value::scalar(22f64),
Value::scalar(23f64),
Value::scalar(24f64),
Value::scalar("wat".to_owned()),
]),
);
let output = template.render(&runtime).unwrap();
assert_eq!(output, "test 22 test 23 test 24 test wat ");
}
#[test]
fn loop_over_range_literals() {
let text = concat!(
"{% for name in (42..46) %}",
"#{{forloop.index}} test {{name}} | ",
"{% endfor %}",
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
"#1 test 42 | #2 test 43 | #3 test 44 | #4 test 45 | #5 test 46 | "
);
}
#[test]
fn loop_over_range_vars() {
let text = concat!(
"{% for x in (alpha .. omega) %}",
"#{{forloop.index}} test {{x}}, ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("alpha".into(), Value::scalar(42i64));
runtime.set_global("omega".into(), Value::scalar(46i64));
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
"#1 test 42, #2 test 43, #3 test 44, #4 test 45, #5 test 46, "
);
}
#[test]
fn nested_forloops() {
let text = concat!(
"{% for outer in (1..5) %}",
">>{{forloop.index0}}:{{outer}}>>",
"{% for inner in (6..10) %}",
"{{outer}}:{{forloop.index0}}:{{inner}},",
"{% endfor %}",
">>{{outer}}>>\n",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
concat!(
">>0:1>>1:0:6,1:1:7,1:2:8,1:3:9,1:4:10,>>1>>\n",
">>1:2>>2:0:6,2:1:7,2:2:8,2:3:9,2:4:10,>>2>>\n",
">>2:3>>3:0:6,3:1:7,3:2:8,3:3:9,3:4:10,>>3>>\n",
">>3:4>>4:0:6,4:1:7,4:2:8,4:3:9,4:4:10,>>4>>\n",
">>4:5>>5:0:6,5:1:7,5:2:8,5:3:9,5:4:10,>>5>>\n",
)
);
}
#[test]
fn nested_forloops_with_else() {
let text = concat!(
"{% for x in i %}",
"{% for y in j %}inner{% else %}empty inner{% endfor %}",
"{% else %}",
"empty outer",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("i".into(), Value::Array(vec![]));
runtime.set_global("j".into(), Value::Array(vec![]));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "empty outer");
runtime.set_global("i".into(), Value::Array(vec![Value::scalar(1i64)]));
runtime.set_global("j".into(), Value::Array(vec![]));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "empty inner");
}
#[test]
fn degenerate_range_is_safe() {
let text = concat!("{% for x in (10 .. 0) %}", "{{x}}", "{% endfor %}");
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "");
}
#[test]
fn limited_loop() {
let text = concat!(
"{% for i in (1..100) limit:2 %}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "1 2 ");
}
#[test]
fn offset_loop() {
let text = concat!(
"{% for i in (1..10) offset:4 %}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "5 6 7 8 9 10 ");
}
#[test]
fn offset_and_limited_loop() {
let text = concat!(
"{% for i in (1..10) offset:4 limit:2 %}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "5 6 ");
}
#[test]
fn reversed_loop() {
let text = concat!(
"{% for i in (1..10) reversed %}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "10 9 8 7 6 5 4 3 2 1 ");
}
#[test]
fn sliced_and_reversed_loop() {
let text = concat!(
"{% for i in (1..10) reversed offset:1 limit:5%}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "6 5 4 3 2 ");
}
#[test]
fn empty_loop_invokes_else_template() {
let text = concat!(
"{% for i in (1..10) limit:0 %}",
"{{ i }} ",
"{% else %}",
"There are no items!",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "There are no items!");
}
#[test]
fn nil_loop_invokes_else_template() {
let text = concat!(
"{% for i in nil %}",
"{{ i }} ",
"{% else %}",
"There are no items!",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "There are no items!");
}
#[test]
fn limit_greater_than_iterator_length() {
let text = concat!("{% for i in (1..5) limit:10 %}", "{{ i }} ", "{% endfor %}");
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "1 2 3 4 5 ");
}
#[test]
fn loop_variables() {
let text = concat!(
"{% for v in (100..102) %}",
"length: {{forloop.length}}, ",
"index: {{forloop.index}}, ",
"index0: {{forloop.index0}}, ",
"rindex: {{forloop.rindex}}, ",
"rindex0: {{forloop.rindex0}}, ",
"value: {{v}}, ",
"first: {{forloop.first}}, ",
"last: {{forloop.last}}\n",
"{% endfor %}",
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
concat!(
"length: 3, index: 1, index0: 0, rindex: 3, rindex0: 2, value: 100, first: true, last: false\n",
"length: 3, index: 2, index0: 1, rindex: 2, rindex0: 1, value: 101, first: false, last: false\n",
"length: 3, index: 3, index0: 2, rindex: 1, rindex0: 0, value: 102, first: false, last: true\n",
)
);
}
#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(name = "shout", description = "tests helper", parsed(ShoutFilter))]
pub struct ShoutFilterParser;
#[derive(Debug, Default, Display_filter)]
#[name = "shout"]
pub struct ShoutFilter;
impl Filter for ShoutFilter {
fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
Ok(Value::scalar(input.to_kstr().to_uppercase()))
}
}
#[test]
fn use_filters() {
let text = concat!(
"{% for name in array %}",
"test {{name | shout}} ",
"{% endfor %}",
);
let mut options = options();
options
.filters
.register("shout".to_string(), Box::new(ShoutFilterParser));
let template = parser::parse(text, &options)
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"array".into(),
Value::Array(vec![
Value::scalar("alpha"),
Value::scalar("beta"),
Value::scalar("gamma"),
]),
);
let output = template.render(&runtime).unwrap();
assert_eq!(output, "test ALPHA test BETA test GAMMA ");
}
#[test]
fn for_loop_parameters_with_variables() {
let text = concat!(
"{% assign l = 4 %}",
"{% assign o = 5 %}",
"{% for i in (1..100) limit:l offset:o %}",
"{{ i }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(output, "6 7 8 9 ");
}
#[test]
fn tablerow_without_cols() {
let text = concat!(
"{% tablerow name in array %}",
"test {{name}} ",
"{% endtablerow %}",
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"array".into(),
Value::Array(vec![
Value::scalar(22f64),
Value::scalar(23f64),
Value::scalar(24f64),
Value::scalar("wat".to_owned()),
]),
);
let output = template.render(&runtime).unwrap();
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>");
}
#[test]
fn tablerow_with_cols() {
let text = concat!(
"{% tablerow name in (42..46) cols:2 %}",
"test {{name}} ",
"{% endtablerow %}",
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"array".into(),
Value::Array(vec![
Value::scalar(22f64),
Value::scalar(23f64),
Value::scalar(24f64),
Value::scalar("wat".to_owned()),
]),
);
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
"<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>"
);
}
#[test]
fn tablerow_loop_parameters_with_variables() {
let text = concat!(
"{% assign l = 4 %}",
"{% assign o = 5 %}",
"{% assign c = 3 %}",
"{% tablerow i in (1..100) limit:l offset:o cols:c %}",
"{{ i }} ",
"{% endtablerow %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
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>");
}
#[test]
fn tablerow_variables() {
let text = concat!(
"{% tablerow v in (100..103) cols:2 %}",
"length: {{tablerow.length}}, ",
"index: {{tablerow.index}}, ",
"index0: {{tablerow.index0}}, ",
"rindex: {{tablerow.rindex}}, ",
"rindex0: {{tablerow.rindex0}}, ",
"col: {{tablerow.col}}, ",
"col0: {{tablerow.col0}}, ",
"value: {{v}}, ",
"first: {{tablerow.first}}, ",
"last: {{tablerow.last}}, ",
"col_first: {{tablerow.col_first}}, ",
"col_last: {{tablerow.col_last}}",
"{% endtablerow %}",
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let output = template.render(&runtime).unwrap();
assert_eq!(
output,
concat!(
"<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>",
"<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>",
"<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>",
"<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>",
)
);
}
#[test]
fn test_for_parentloop_nil_when_not_present() {
let text = concat!(
"{% for inner in outer %}",
"{{ forloop.parentloop }}.{{ forloop.index }} ",
"{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"outer".into(),
Value::Array(vec![
Value::Array(vec![
Value::scalar(1f64),
Value::scalar(1f64),
Value::scalar(1f64),
]),
Value::Array(vec![
Value::scalar(1f64),
Value::scalar(1f64),
Value::scalar(1f64),
]),
]),
);
let output = template.render(&runtime).unwrap();
assert_eq!(output, ".1 .2 ");
}
#[test]
fn test_for_parentloop_references_parent_loop() {
let text = concat!(
"{% for inner in outer %}{% for k in inner %}",
"{{ forloop.parentloop.index }}.{{ forloop.index }} ",
"{% endfor %}{% endfor %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global(
"outer".into(),
Value::Array(vec![
Value::Array(vec![
Value::scalar(1f64),
Value::scalar(1f64),
Value::scalar(1f64),
]),
Value::Array(vec![
Value::scalar(1f64),
Value::scalar(1f64),
Value::scalar(1f64),
]),
]),
);
let output = template.render(&runtime).unwrap();
assert_eq!(output, "1.1 1.2 1.3 2.1 2.2 2.3 ");
}
}