use std::fmt;
use std::io::Write;
use liquid_core::error::ResultLiquidExt;
use liquid_core::model::{ValueView, ValueViewCmp};
use liquid_core::parser::BlockElement;
use liquid_core::parser::TagToken;
use liquid_core::Expression;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Runtime;
use liquid_core::Template;
use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter};
use liquid_core::{Error, Result};
#[derive(Copy, Clone, Debug, Default)]
pub struct IfBlock;
impl IfBlock {
pub fn new() -> Self {
Self::default()
}
}
impl BlockReflection for IfBlock {
fn start_tag(&self) -> &str {
"if"
}
fn end_tag(&self) -> &str {
"endif"
}
fn description(&self) -> &str {
""
}
}
impl ParseBlock for IfBlock {
fn parse(
&self,
arguments: TagTokenIter<'_>,
mut tokens: TagBlock<'_, '_>,
options: &Language,
) -> Result<Box<dyn Renderable>> {
let conditional = parse_if(arguments, &mut tokens, options)?;
tokens.assert_empty();
Ok(conditional)
}
fn reflection(&self) -> &dyn BlockReflection {
self
}
}
fn parse_if(
arguments: TagTokenIter<'_>,
tokens: &mut TagBlock<'_, '_>,
options: &Language,
) -> Result<Box<dyn Renderable>> {
let condition = parse_condition(arguments)?;
let mut if_true = Vec::new();
let mut if_false = None;
while let Some(element) = tokens.next()? {
match element {
BlockElement::Tag(tag) => match tag.name() {
"else" => {
if_false = Some(tokens.parse_all(options)?);
break;
}
"elsif" => {
if_false = Some(vec![parse_if(tag.into_tokens(), tokens, options)?]);
break;
}
_ => if_true.push(tag.parse(tokens, options)?),
},
element => if_true.push(element.parse(tokens, options)?),
}
}
let if_true = Template::new(if_true);
let if_false = if_false.map(Template::new);
Ok(Box::new(Conditional {
condition,
mode: true,
if_true,
if_false,
}))
}
#[derive(Copy, Clone, Debug, Default)]
pub struct UnlessBlock;
impl UnlessBlock {
pub fn new() -> Self {
Self::default()
}
}
impl BlockReflection for UnlessBlock {
fn start_tag(&self) -> &str {
"unless"
}
fn end_tag(&self) -> &str {
"endunless"
}
fn description(&self) -> &str {
""
}
}
impl ParseBlock for UnlessBlock {
fn parse(
&self,
arguments: TagTokenIter<'_>,
mut tokens: TagBlock<'_, '_>,
options: &Language,
) -> Result<Box<dyn Renderable>> {
let condition = parse_condition(arguments)?;
let mut if_true = Vec::new();
let mut if_false = None;
while let Some(element) = tokens.next()? {
match element {
BlockElement::Tag(tag) => match tag.name() {
"else" => {
if_false = Some(tokens.parse_all(options)?);
break;
}
_ => if_true.push(tag.parse(&mut tokens, options)?),
},
element => if_true.push(element.parse(&mut tokens, options)?),
}
}
let if_true = Template::new(if_true);
let if_false = if_false.map(Template::new);
tokens.assert_empty();
Ok(Box::new(Conditional {
condition,
mode: false,
if_true,
if_false,
}))
}
fn reflection(&self) -> &dyn BlockReflection {
self
}
}
#[derive(Debug)]
struct Conditional {
condition: Condition,
mode: bool,
if_true: Template,
if_false: Option<Template>,
}
impl Conditional {
fn compare(&self, runtime: &dyn Runtime) -> Result<bool> {
let result = self.condition.evaluate(runtime)?;
Ok(result == self.mode)
}
fn trace(&self) -> String {
format!("{{% if {} %}}", self.condition)
}
}
impl Renderable for Conditional {
fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
let condition = self.compare(runtime).trace_with(|| self.trace().into())?;
if condition {
self.if_true
.render_to(writer, runtime)
.trace_with(|| self.trace().into())?;
} else if let Some(ref template) = self.if_false {
template
.render_to(writer, runtime)
.trace("{{% else %}}")
.trace_with(|| self.trace().into())?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
enum Condition {
Binary(BinaryCondition),
Existence(ExistenceCondition),
Conjunction(Box<Condition>, Box<Condition>),
Disjunction(Box<Condition>, Box<Condition>),
}
impl Condition {
pub fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
match *self {
Condition::Binary(ref c) => c.evaluate(runtime),
Condition::Existence(ref c) => c.evaluate(runtime),
Condition::Conjunction(ref left, ref right) => {
Ok(left.evaluate(runtime)? && right.evaluate(runtime)?)
}
Condition::Disjunction(ref left, ref right) => {
Ok(left.evaluate(runtime)? || right.evaluate(runtime)?)
}
}
}
}
impl fmt::Display for Condition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Condition::Binary(ref c) => write!(f, "{}", c),
Condition::Existence(ref c) => write!(f, "{}", c),
Condition::Conjunction(ref left, ref right) => write!(f, "{} and {}", left, right),
Condition::Disjunction(ref left, ref right) => write!(f, "{} or {}", left, right),
}
}
}
#[derive(Clone, Debug)]
struct BinaryCondition {
lh: Expression,
comparison: ComparisonOperator,
rh: Expression,
}
impl BinaryCondition {
pub fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
let a = self.lh.evaluate(runtime)?;
let ca = ValueViewCmp::new(a.as_view());
let b = self.rh.evaluate(runtime)?;
let cb = ValueViewCmp::new(b.as_view());
let result = match self.comparison {
ComparisonOperator::Equals => ca == cb,
ComparisonOperator::NotEquals => ca != cb,
ComparisonOperator::LessThan => ca < cb,
ComparisonOperator::GreaterThan => ca > cb,
ComparisonOperator::LessThanEquals => ca <= cb,
ComparisonOperator::GreaterThanEquals => ca >= cb,
ComparisonOperator::Contains => contains_check(a.as_view(), b.as_view())?,
};
Ok(result)
}
}
impl fmt::Display for BinaryCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.lh, self.comparison, self.rh)
}
}
fn contains_check(a: &dyn ValueView, b: &dyn ValueView) -> Result<bool> {
if let Some(a) = a.as_scalar() {
let b = b.to_kstr();
Ok(a.to_kstr().contains(b.as_str()))
} else if let Some(a) = a.as_object() {
let b = b.as_scalar();
let check = b
.map(|b| a.contains_key(b.to_kstr().as_str()))
.unwrap_or(false);
Ok(check)
} else if let Some(a) = a.as_array() {
for elem in a.values() {
if ValueViewCmp::new(elem) == ValueViewCmp::new(b) {
return Ok(true);
}
}
Ok(false)
} else {
Err(unexpected_value_error(
"string | array | object",
Some(a.type_name()),
))
}
}
#[derive(Clone, Debug)]
enum ComparisonOperator {
Equals,
NotEquals,
LessThan,
GreaterThan,
LessThanEquals,
GreaterThanEquals,
Contains,
}
impl fmt::Display for ComparisonOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let out = match *self {
ComparisonOperator::Equals => "==",
ComparisonOperator::NotEquals => "!=",
ComparisonOperator::LessThanEquals => "<=",
ComparisonOperator::GreaterThanEquals => ">=",
ComparisonOperator::LessThan => "<",
ComparisonOperator::GreaterThan => ">",
ComparisonOperator::Contains => "contains",
};
write!(f, "{}", out)
}
}
impl ComparisonOperator {
fn from_str(s: &str) -> ::std::result::Result<Self, ()> {
match s {
"==" => Ok(ComparisonOperator::Equals),
"!=" | "<>" => Ok(ComparisonOperator::NotEquals),
"<" => Ok(ComparisonOperator::LessThan),
">" => Ok(ComparisonOperator::GreaterThan),
"<=" => Ok(ComparisonOperator::LessThanEquals),
">=" => Ok(ComparisonOperator::GreaterThanEquals),
"contains" => Ok(ComparisonOperator::Contains),
_ => Err(()),
}
}
}
#[derive(Clone, Debug)]
struct ExistenceCondition {
lh: Expression,
}
impl ExistenceCondition {
pub fn evaluate(&self, runtime: &dyn Runtime) -> Result<bool> {
let a = self.lh.try_evaluate(runtime);
let a = a.unwrap_or_default();
let is_truthy = a.query_state(liquid_core::model::State::Truthy);
Ok(is_truthy)
}
}
impl fmt::Display for ExistenceCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.lh)
}
}
struct PeekableTagTokenIter<'a> {
iter: TagTokenIter<'a>,
peeked: Option<Option<TagToken<'a>>>,
}
impl<'a> Iterator for PeekableTagTokenIter<'a> {
type Item = TagToken<'a>;
fn next(&mut self) -> Option<TagToken<'a>> {
match self.peeked.take() {
Some(v) => v,
None => self.iter.next(),
}
}
}
impl<'a> PeekableTagTokenIter<'a> {
pub fn expect_next(&mut self, error_msg: &str) -> Result<TagToken<'a>> {
self.next().ok_or_else(|| self.iter.raise_error(error_msg))
}
fn peek(&mut self) -> Option<&TagToken<'a>> {
if self.peeked.is_none() {
self.peeked = Some(self.iter.next());
}
match self.peeked {
Some(Some(ref value)) => Some(value),
Some(None) => None,
None => unreachable!(),
}
}
}
fn parse_atom_condition(arguments: &mut PeekableTagTokenIter<'_>) -> Result<Condition> {
let lh = arguments
.expect_next("Value expected.")?
.expect_value()
.into_result()?;
let cond = match arguments
.peek()
.map(TagToken::as_str)
.and_then(|op| ComparisonOperator::from_str(op).ok())
{
Some(op) => {
arguments.next();
let rh = arguments
.expect_next("Value expected.")?
.expect_value()
.into_result()?;
Condition::Binary(BinaryCondition {
lh,
comparison: op,
rh,
})
}
None => Condition::Existence(ExistenceCondition { lh }),
};
Ok(cond)
}
fn parse_conjunction_chain(arguments: &mut PeekableTagTokenIter<'_>) -> Result<Condition> {
let mut lh = parse_atom_condition(arguments)?;
while let Some("and") = arguments.peek().map(TagToken::as_str) {
arguments.next();
let rh = parse_atom_condition(arguments)?;
lh = Condition::Conjunction(Box::new(lh), Box::new(rh));
}
Ok(lh)
}
fn parse_condition(arguments: TagTokenIter<'_>) -> Result<Condition> {
let mut arguments = PeekableTagTokenIter {
iter: arguments,
peeked: None,
};
let mut lh = parse_conjunction_chain(&mut arguments)?;
while let Some(token) = arguments.next() {
token
.expect_str("or")
.into_result_custom_msg("\"and\" or \"or\" expected.")?;
let rh = parse_conjunction_chain(&mut arguments)?;
lh = Condition::Disjunction(Box::new(lh), Box::new(rh));
}
Ok(lh)
}
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 super::*;
use liquid_core::model::Object;
use liquid_core::model::Value;
use liquid_core::parser;
use liquid_core::runtime;
use liquid_core::runtime::RuntimeBuilder;
fn options() -> Language {
let mut options = Language::default();
options.blocks.register("if".to_string(), IfBlock.into());
options
.blocks
.register("unless".to_string(), UnlessBlock.into());
options
}
#[test]
fn number_comparison() {
let text = "{% if 6 < 7 %}if true{% endif %}";
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, "if true");
let text = "{% if 7 < 6 %}if true{% else %}if false{% endif %}";
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, "if false");
}
#[test]
fn string_comparison() {
let text = r#"{% if "one" == "one" %}if true{% endif %}"#;
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, "if true");
let text = r#"{% if "one" == "two" %}if true{% else %}if false{% endif %}"#;
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, "if false");
}
#[test]
fn implicit_comparison() {
let text = concat!(
"{% if truthy %}",
"yep",
"{% else %}",
"nope",
"{% endif %}"
);
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, "nope");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("truthy".into(), Value::Nil);
let output = template.render(&runtime).unwrap();
assert_eq!(output, "nope");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("truthy".into(), Value::scalar(false));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "nope");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("truthy".into(), Value::scalar(true));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "yep");
}
#[test]
fn unless() {
let text = concat!(
"{% unless some_value == 1 %}",
"unless body",
"{% endunless %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("some_value".into(), Value::scalar(1f64));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("some_value".into(), Value::scalar(42f64));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "unless body");
}
#[test]
fn nested_if_else() {
let text = concat!(
"{% if truthy %}",
"yep, ",
"{% if also_truthy %}",
"also truthy",
"{% else %}",
"not also truthy",
"{% endif %}",
"{% else %}",
"nope",
"{% endif %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("truthy".into(), Value::scalar(true));
runtime.set_global("also_truthy".into(), Value::scalar(false));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "yep, not also truthy");
}
#[test]
fn multiple_elif_blocks() {
let text = concat!(
"{% if a == 1 %}",
"first",
"{% elsif a == 2 %}",
"second",
"{% elsif a == 3 %}",
"third",
"{% else %}",
"fourth",
"{% endif %}"
);
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("a".into(), Value::scalar(1f64));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "first");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("a".into(), Value::scalar(2f64));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "second");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("a".into(), Value::scalar(3f64));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "third");
let runtime = RuntimeBuilder::new().build();
runtime.set_global("a".into(), Value::scalar("else"));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "fourth");
}
#[test]
fn string_contains_with_literals() {
let text = "{% if \"Star Wars\" contains \"Star\" %}if true{% endif %}";
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, "if true");
let text = "{% if \"Star Wars\" contains \"Alf\" %}if true{% else %}if false{% endif %}";
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, "if false");
}
#[test]
fn string_contains_with_variables() {
let text = "{% if movie contains \"Star\" %}if true{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("movie".into(), Value::scalar("Star Wars"));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if true");
let text = "{% if movie contains \"Star\" %}if true{% else %}if false{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
runtime.set_global("movie".into(), Value::scalar("Batman"));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if false");
}
#[test]
fn contains_with_object_and_key() {
let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let mut obj = Object::new();
obj.insert("Star Wars".into(), Value::scalar("1977"));
runtime.set_global("movies".into(), Value::Object(obj));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if true");
}
#[test]
fn contains_with_object_and_missing_key() {
let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let obj = Object::new();
runtime.set_global("movies".into(), Value::Object(obj));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if false");
}
#[test]
fn contains_with_array_and_match() {
let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let arr = vec![
Value::scalar("Star Wars"),
Value::scalar("Star Trek"),
Value::scalar("Alien"),
];
runtime.set_global("movies".into(), Value::Array(arr));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if true");
}
#[test]
fn contains_with_array_and_no_match() {
let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
let template = parser::parse(text, &options())
.map(runtime::Template::new)
.unwrap();
let runtime = RuntimeBuilder::new().build();
let arr = vec![Value::scalar("Alien")];
runtime.set_global("movies".into(), Value::Array(arr));
let output = template.render(&runtime).unwrap();
assert_eq!(output, "if false");
}
#[test]
fn multiple_conditions_and() {
let text = "{% if 1 == 1 and 2 == 2 %}if true{% else %}if false{% endif %}";
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, "if true");
let text = "{% if 1 == 1 and 2 != 2 %}if true{% else %}if false{% endif %}";
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, "if false");
}
#[test]
fn multiple_conditions_or() {
let text = "{% if 1 == 1 or 2 != 2 %}if true{% else %}if false{% endif %}";
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, "if true");
let text = "{% if 1 != 1 or 2 != 2 %}if true{% else %}if false{% endif %}";
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, "if false");
}
#[test]
fn multiple_conditions_and_or() {
let text = "{% if 1 == 1 or 2 == 2 and 3 != 3 %}if true{% else %}if false{% endif %}";
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, "if true");
}
}