Skip to content

Commit

Permalink
feat(grammar): Support for empty/blank literals
Browse files Browse the repository at this point in the history
Fixes #222

BREAKING CHANGE: `empty` and `blank` can no longer be variable names.
  • Loading branch information
epage committed Dec 14, 2018
1 parent 7d3b0e5 commit ef72181
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 27 deletions.
4 changes: 3 additions & 1 deletion liquid-compiler/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ FilterChain = { Value ~ ("|" ~ Filter)* }

// Literals
NilLiteral = @{ "nil" | "null" }
EmptyLiteral = @{ "empty" }
BlankLiteral = @{ "blank" }
StringLiteral = @{ ("'" ~ (!"'" ~ ANY)* ~ "'")
| ("\"" ~ (!"\"" ~ ANY)* ~ "\"") }

Expand All @@ -43,7 +45,7 @@ FloatLiteral = @{ ("+" | "-")? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }

BooleanLiteral = @{ "true" | "false" }

Literal = { NilLiteral | StringLiteral | FloatLiteral | IntegerLiteral | BooleanLiteral }
Literal = { NilLiteral | EmptyLiteral | BlankLiteral | StringLiteral | FloatLiteral | IntegerLiteral | BooleanLiteral }

Range = { "(" ~ Value ~ ".." ~ Value ~ ")" }

Expand Down
18 changes: 15 additions & 3 deletions liquid-compiler/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ fn parse_literal(literal: Pair) -> Value {

match literal.as_rule() {
Rule::NilLiteral => Value::Nil,
Rule::EmptyLiteral => Value::Empty,
Rule::BlankLiteral => Value::Blank,
Rule::StringLiteral => {
let literal = literal.as_str();
let trim_quotes = &literal[1..literal.len() - 1];
Expand Down Expand Up @@ -871,6 +873,18 @@ mod test {
.unwrap();
assert_eq!(parse_literal(nil), Value::Nil);

let blank = LiquidParser::parse(Rule::Literal, "blank")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(blank), Value::Blank);

let empty = LiquidParser::parse(Rule::Literal, "empty")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(empty), Value::Empty);

let integer = LiquidParser::parse(Rule::Literal, "42")
.unwrap()
.next()
Expand Down Expand Up @@ -942,9 +956,7 @@ mod test {
let options = LiquidOptions::default();

let mut context = Context::new();
context
.stack_mut()
.set_global("exp", Value::scalar(5));
context.stack_mut().set_global("exp", Value::scalar(5));

let text = " \n {{ exp }} \n ";
let template = parse(text, &options).map(Template::new).unwrap();
Expand Down
1 change: 1 addition & 0 deletions liquid-value/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extern crate itertools;
extern crate liquid_error;
extern crate num_traits;

#[macro_use]
mod macros;

pub mod map;
Expand Down
18 changes: 9 additions & 9 deletions liquid-value/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,32 +191,32 @@ macro_rules! value_internal {
//////////////////////////////////////////////////////////////////////////

(nil) => {
::liquid_value::Value::Nil
$crate::Value::Nil
};

(true) => {
::liquid_value::Value::scalar(true)
$crate::Value::scalar(true)
};

(false) => {
::liquid_value::Value::scalar(false)
$crate::Value::scalar(false)
};

([]) => {
::liquid_value::Value::Array(value_internal_vec![])
$crate::Value::Array(value_internal_vec![])
};

([ $($tt:tt)+ ]) => {
::liquid_value::Value::Array(value_internal!(@array [] $($tt)+))
$crate::Value::Array(value_internal!(@array [] $($tt)+))
};

({}) => {
::liquid_value::Value::Object(Default::default())
$crate::Value::Object(Default::default())
};

({ $($tt:tt)+ }) => {
::liquid_value::Value::Object({
let mut object = ::liquid_value::Object::new();
$crate::Value::Object({
let mut object = $crate::Object::new();
value_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
Expand All @@ -229,7 +229,7 @@ macro_rules! value_internal {
// Any Serialize type: numbers, strings, struct literals, variables etc.
// Must be below every other rule.
($other:expr) => {
::liquid_value::to_value($other).unwrap()
$crate::to_value($other).unwrap()
};
}

Expand Down
106 changes: 103 additions & 3 deletions liquid-value/src/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub enum Value {
Object(Object),
/// Nothing.
Nil,
/// No content.
Empty,
/// Evaluates to empty string.
Blank,
}

/// Type representing a Liquid array, payload of the `Value::Array` variant
Expand Down Expand Up @@ -55,7 +59,7 @@ impl Value {
let arr: Vec<String> = x.iter().map(|(k, v)| format!("{}: {}", k, v)).collect();
borrow::Cow::Owned(arr.join(", "))
}
Value::Nil => borrow::Cow::Borrowed(""),
Value::Nil | Value::Empty | Value::Blank => borrow::Cow::Borrowed(""),
}
}

Expand Down Expand Up @@ -154,12 +158,44 @@ impl Value {
}
}

/// Extracts the empty value if it is empty
pub fn as_empty(&self) -> Option<()> {
match *self {
Value::Empty => Some(()),
_ => None,
}
}

/// Tests whether this value is empty
pub fn is_empty(&self) -> bool {
match *self {
Value::Empty => true,
_ => false,
}
}

/// Extracts the blank value if it is blank
pub fn as_blank(&self) -> Option<()> {
match *self {
Value::Blank => Some(()),
_ => None,
}
}

/// Tests whether this value is blank
pub fn is_blank(&self) -> bool {
match *self {
Value::Blank => true,
_ => false,
}
}

/// Evaluate using Liquid "truthiness"
pub fn is_truthy(&self) -> bool {
// encode Ruby truthiness: all values except false and nil are true
match *self {
Value::Scalar(ref x) => x.is_truthy(),
Value::Nil => false,
Value::Nil | Value::Empty | Value::Blank => false,
_ => true,
}
}
Expand All @@ -169,6 +205,8 @@ impl Value {
match *self {
Value::Scalar(ref x) => x.is_default(),
Value::Nil => true,
Value::Empty => true,
Value::Blank => true,
Value::Array(ref x) => x.is_empty(),
Value::Object(ref x) => x.is_empty(),
}
Expand All @@ -179,6 +217,8 @@ impl Value {
match *self {
Value::Scalar(ref x) => x.type_name(),
Value::Nil => "nil",
Value::Empty => "empty",
Value::Blank => "blank",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
Expand Down Expand Up @@ -316,7 +356,36 @@ fn value_eq(lhs: &Value, rhs: &Value) -> bool {
(&Value::Scalar(ref x), &Value::Scalar(ref y)) => x == y,
(&Value::Array(ref x), &Value::Array(ref y)) => x == y,
(&Value::Object(ref x), &Value::Object(ref y)) => x == y,
(&Value::Nil, &Value::Nil) => true,
(&Value::Nil, &Value::Nil)
| (&Value::Empty, &Value::Empty)
| (&Value::Blank, &Value::Blank)
| (&Value::Empty, &Value::Blank)
| (&Value::Blank, &Value::Empty) => true,

// encode a best-guess of empty rules
// See tables in https://stackoverflow.com/questions/885414/a-concise-explanation-of-nil-v-empty-v-blank-in-ruby-on-rails
(&Value::Empty, &Value::Scalar(ref s)) | (&Value::Scalar(ref s), &Value::Empty) => {
s.to_str().is_empty()
}
(&Value::Empty, &Value::Array(ref s)) | (&Value::Array(ref s), &Value::Empty) => {
s.is_empty()
}
(&Value::Empty, &Value::Object(ref s)) | (&Value::Object(ref s), &Value::Empty) => {
s.is_empty()
}

// encode a best-guess of blank rules
// See tables in https://stackoverflow.com/questions/885414/a-concise-explanation-of-nil-v-empty-v-blank-in-ruby-on-rails
(&Value::Nil, &Value::Blank) | (&Value::Blank, &Value::Nil) => true,
(&Value::Blank, &Value::Scalar(ref s)) | (&Value::Scalar(ref s), &Value::Blank) => {
s.to_str().trim().is_empty() || !s.to_bool().unwrap_or(true)
}
(&Value::Blank, &Value::Array(ref s)) | (&Value::Array(ref s), &Value::Blank) => {
s.is_empty()
}
(&Value::Blank, &Value::Object(ref s)) | (&Value::Object(ref s), &Value::Blank) => {
s.is_empty()
}

// encode Ruby truthiness: all values except false and nil are true
(&Value::Nil, &Value::Scalar(ref b)) | (&Value::Scalar(ref b), &Value::Nil) => {
Expand Down Expand Up @@ -445,4 +514,35 @@ mod test {
assert!(Value::scalar(true) != Value::Nil);
assert!(Value::scalar("") != Value::Nil);
}

#[test]
fn empty_equality() {
// Truth table from https://stackoverflow.com/questions/885414/a-concise-explanation-of-nil-v-empty-v-blank-in-ruby-on-rails
assert_eq!(Value::Empty, Value::Empty);
assert_eq!(Value::Empty, Value::Blank);
assert_eq!(Value::Empty, liquid_value!(""));
assert_ne!(Value::Empty, liquid_value!(" "));
assert_eq!(Value::Empty, liquid_value!([]));
assert_ne!(Value::Empty, liquid_value!([nil]));
assert_eq!(Value::Empty, liquid_value!({}));
assert_ne!(Value::Empty, liquid_value!({ "a": nil }));
}

#[test]
fn blank_equality() {
// Truth table from https://stackoverflow.com/questions/885414/a-concise-explanation-of-nil-v-empty-v-blank-in-ruby-on-rails
assert_eq!(Value::Blank, Value::Blank);
assert_eq!(Value::Blank, Value::Empty);
assert_eq!(Value::Blank, liquid_value!(nil));
assert_eq!(Value::Blank, liquid_value!(false));
assert_ne!(Value::Blank, liquid_value!(true));
assert_ne!(Value::Blank, liquid_value!(0));
assert_ne!(Value::Blank, liquid_value!(1));
assert_eq!(Value::Blank, liquid_value!(""));
assert_eq!(Value::Blank, liquid_value!(" "));
assert_eq!(Value::Blank, liquid_value!([]));
assert_ne!(Value::Blank, liquid_value!([nil]));
assert_eq!(Value::Blank, liquid_value!({}));
assert_ne!(Value::Blank, liquid_value!({ "a": nil }));
}
}
1 change: 1 addition & 0 deletions src/tags/for_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ impl For {
fn get_array(context: &Context, array_id: &Expression) -> Result<Vec<Value>> {
let array = array_id.evaluate(context)?;
match array {
Value::Empty => Ok(vec![]),
Value::Array(x) => Ok(x.to_owned()),
Value::Object(x) => {
let x = x
Expand Down
1 change: 0 additions & 1 deletion tests/conformance_ruby/tags/for_tag_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ fn test_for_parentloop_nil_when_not_present() {
}

#[test]
#[should_panic] // liquid-rust#222
fn test_inner_for_over_empty_input() {
assert_template_result!(
"oo",
Expand Down
10 changes: 4 additions & 6 deletions tests/conformance_ruby/tags/standard_tag_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,11 @@ fn test_size_of_hash() {
}

#[test]
#[should_panic] // liquid-rust#222
fn test_illegal_symbols() {
// Implementation specific: strict_variables is enabled, testing that instead.
assert_render_error!("{% if true == empty %}?{% endif %}");
assert_render_error!("{% if true == null %}?{% endif %}");
assert_render_error!("{% if empty == true %}?{% endif %}");
assert_render_error!("{% if null == true %}?{% endif %}");
assert_template_result!("", "{% if true == empty %}?{% endif %}");
assert_template_result!("", "{% if true == null %}?{% endif %}");
assert_template_result!("", "{% if empty == true %}?{% endif %}");
assert_template_result!("", "{% if null == true %}?{% endif %}");
}

#[test]
Expand Down
2 changes: 0 additions & 2 deletions tests/conformance_ruby/tags/statements_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,12 @@ fn test_var_and_long_string_are_equal_backwards() {
*/

#[test]
#[should_panic] // liquid-rust#222
fn test_is_collection_empty() {
let text = " {% if array == empty %} true {% else %} false {% endif %} ";
assert_template_result!(" true ", text, v!({"array": []}));
}

#[test]
#[should_panic] // liquid-rust#222
fn test_is_not_collection_empty() {
let text = " {% if array == empty %} true {% else %} false {% endif %} ";
assert_template_result!(" false ", text, v!({"array": [1, 2, 3]}));
Expand Down
2 changes: 0 additions & 2 deletions tests/conformance_ruby/variable_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@ fn test_ignore_unknown() {
}

#[test]
#[should_panic] // liquid-rust#222
fn test_using_blank_as_variable_name() {
assert_template_result!(r#""#, r#"{% assign foo = blank %}{{ foo }}"#);
}

#[test]
#[should_panic] // liquid-rust#222
fn test_using_empty_as_variable_name() {
assert_template_result!(r#""#, r#"{% assign foo = empty %}{{ foo }}"#);
}
Expand Down

0 comments on commit ef72181

Please sign in to comment.