Skip to content

Commit

Permalink
feat(grammar): Support for nil literals
Browse files Browse the repository at this point in the history
Fixes #223

BREAKING CHANGE: Templates with `null` and `nil` variable names will be
broken.
  • Loading branch information
epage committed Dec 13, 2018
1 parent 3735e7e commit 7d3b0e5
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 39 deletions.
25 changes: 13 additions & 12 deletions liquid-compiler/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ NON_WHITESPACE_CONTROL_HYPHEN = _{ !"-}}" ~ !"-%}" ~ "-" }
LiquidFile = ${ SOI ~ Element* ~ EOI }

// Element-level parsing
Element = _{ Expression | Tag | Raw }
Element = _{ Expression | Tag | Raw }

TagStart = _{ (WHITESPACE* ~ "{%-") | "{%" }
TagEnd = _{ ("-%}" ~ WHITESPACE*) | "%}" }
Expand All @@ -14,39 +14,40 @@ ExpressionInner = !{FilterChain}

Tag = { TagStart ~ WHITESPACE* ~ TagInner ~ WHITESPACE* ~ TagEnd }
Expression = { ExpressionStart ~ WHITESPACE* ~ ExpressionInner ~ WHITESPACE* ~ ExpressionEnd }
// Not allowing Tag/Expression Start/End might become a problem
// Not allowing Tag/Expression Start/End might become a problem
// for {% raw %}, {% comment %} and other blocks that don't parse
// the elements inside with liquid: unclosed delimiters won't be accepted
Raw = @{ (!(TagStart | ExpressionStart | TagEnd | ExpressionEnd) ~ ANY)+ }
Raw = @{ (!(TagStart | ExpressionStart | TagEnd | ExpressionEnd) ~ ANY)+ }


// Inner parsing
// Inner parsing
Identifier = @{ (ASCII_ALPHA | "_" | NON_WHITESPACE_CONTROL_HYPHEN) ~ (ASCII_ALPHANUMERIC | "_" | NON_WHITESPACE_CONTROL_HYPHEN)* }

Variable = ${ Identifier
Variable = ${ Identifier
~ ( ("." ~ Identifier)
| ("[" ~ WHITESPACE* ~ Value ~ WHITESPACE* ~ "]")
)*
)*
}
Value = { Literal | Variable }
Filter = { Identifier ~ (":" ~ Value ~ ("," ~ Value)*)? }
Filter = { Identifier ~ (":" ~ Value ~ ("," ~ Value)*)? }
FilterChain = { Value ~ ("|" ~ Filter)* }


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

IntegerLiteral = @{ ("+" | "-")? ~ ASCII_DIGIT+ }
FloatLiteral = @{ ("+" | "-")? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }

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

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

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

TagToken = _{ Range | FilterChain | DoubleCharSymbol | SingleCharSymbol }
TagToken = _{ Range | FilterChain | DoubleCharSymbol | SingleCharSymbol }

// DoubleCharSymbol must be tried first, otherwise it could be parsed as two SingleCharSymbol instead
SingleCharSymbol = _{ GreaterThan | LesserThan | Assign | Comma | Colon }
Expand All @@ -63,4 +64,4 @@ Equals = { "==" }
NotEquals = { "!=" }
LesserThanGreaterThan = { "<>" }
GreaterThanEquals = { ">=" }
LesserThanEquals = { "<=" }
LesserThanEquals = { "<=" }
49 changes: 30 additions & 19 deletions liquid-compiler/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use liquid_error::{Error, Result};
use liquid_interpreter::Expression;
use liquid_interpreter::Renderable;
use liquid_interpreter::Variable;
use liquid_value::Scalar;
use liquid_value::Value;

use super::LiquidOptions;
Expand Down Expand Up @@ -86,7 +85,7 @@ pub fn parse(text: &str, options: &LiquidOptions) -> Result<Vec<Box<Renderable>>

/// Parses a `Scalar` from a `Pair` with a literal value.
/// This `Pair` must be `Rule::Literal`.
fn parse_literal(literal: Pair) -> Scalar {
fn parse_literal(literal: Pair) -> Value {
if literal.as_rule() != Rule::Literal {
panic!("Expected literal.");
}
Expand All @@ -97,29 +96,30 @@ fn parse_literal(literal: Pair) -> Scalar {
.expect("Get into the rule inside literal.");

match literal.as_rule() {
Rule::NilLiteral => Value::Nil,
Rule::StringLiteral => {
let literal = literal.as_str();
let trim_quotes = &literal[1..literal.len() - 1];

Scalar::new(trim_quotes.to_owned())
Value::scalar(trim_quotes.to_owned())
}
Rule::IntegerLiteral => Scalar::new(
Rule::IntegerLiteral => Value::scalar(
literal
.as_str()
.parse::<i32>()
.expect("Matches are parseable as integers."),
.expect("Grammar ensures matches are parseable as integers."),
),
Rule::FloatLiteral => Scalar::new(
Rule::FloatLiteral => Value::scalar(
literal
.as_str()
.parse::<f64>()
.expect("Matches are parseable as floats."),
.expect("Grammar ensures matches are parseable as floats."),
),
Rule::BooleanLiteral => Scalar::new(
Rule::BooleanLiteral => Value::scalar(
literal
.as_str()
.parse::<bool>()
.expect("Matches are parseable as bools."),
.expect("Grammar ensures matches are parseable as bools."),
),
_ => unreachable!(),
}
Expand Down Expand Up @@ -165,7 +165,7 @@ fn parse_value(value: Pair) -> Expression {
let value = value.into_inner().next().expect("Get inside the value.");

match value.as_rule() {
Rule::Literal => Expression::with_literal(parse_literal(value)),
Rule::Literal => Expression::Literal(parse_literal(value)),
Rule::Variable => Expression::Variable(parse_variable(value)),
_ => unreachable!(),
}
Expand Down Expand Up @@ -812,7 +812,7 @@ impl<'a> TagToken<'a> {
/// The value is returned as a `Value`.
pub fn expect_literal(mut self) -> TryMatchToken<'a, Value> {
match self.unwrap_literal() {
Ok(t) => TryMatchToken::Matches(Value::scalar(parse_literal(t))),
Ok(t) => TryMatchToken::Matches(parse_literal(t)),
Err(_) => {
self.expected.push(Rule::Literal);
TryMatchToken::Fails(self)
Expand Down Expand Up @@ -860,50 +860,61 @@ mod test {

#[test]
fn test_parse_literal() {
let nil = LiquidParser::parse(Rule::Literal, "nil")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(nil), Value::Nil);
let nil = LiquidParser::parse(Rule::Literal, "null")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(nil), Value::Nil);

let integer = LiquidParser::parse(Rule::Literal, "42")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(integer), Scalar::new(42));
assert_eq!(parse_literal(integer), Value::scalar(42));

let negative_int = LiquidParser::parse(Rule::Literal, "-42")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(negative_int), Scalar::new(-42));
assert_eq!(parse_literal(negative_int), Value::scalar(-42));

let float = LiquidParser::parse(Rule::Literal, "4321.032")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(float), Scalar::new(4321.032));
assert_eq!(parse_literal(float), Value::scalar(4321.032));

let negative_float = LiquidParser::parse(Rule::Literal, "-4321.032")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(negative_float), Scalar::new(-4321.032));
assert_eq!(parse_literal(negative_float), Value::scalar(-4321.032));

let boolean = LiquidParser::parse(Rule::Literal, "true")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(boolean), Scalar::new(true));
assert_eq!(parse_literal(boolean), Value::scalar(true));

let string_double_quotes = LiquidParser::parse(Rule::Literal, "\"Hello world!\"")
.unwrap()
.next()
.unwrap();
assert_eq!(
parse_literal(string_double_quotes),
Scalar::new("Hello world!")
Value::scalar("Hello world!")
);

let string_single_quotes = LiquidParser::parse(Rule::Literal, "'Liquid'")
.unwrap()
.next()
.unwrap();
assert_eq!(parse_literal(string_single_quotes), Scalar::new("Liquid"));
assert_eq!(parse_literal(string_single_quotes), Value::scalar("Liquid"));
}

#[test]
Expand Down Expand Up @@ -933,7 +944,7 @@ mod test {
let mut context = Context::new();
context
.stack_mut()
.set_global("exp", Value::Scalar(Scalar::new(5)));
.set_global("exp", Value::scalar(5));

let text = " \n {{ exp }} \n ";
let template = parse(text, &options).map(Template::new).unwrap();
Expand Down
2 changes: 0 additions & 2 deletions tests/conformance_ruby/tags/if_else_tag_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ fn test_if() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_literal_comparisons() {
assert_template_result!(
" NO ",
Expand Down Expand Up @@ -253,7 +252,6 @@ fn test_nested_if() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_comparisons_on_null() {
assert_template_result!("", "{% if null < 10 %} NO {% endif %}");
assert_template_result!("", "{% if null <= 10 %} NO {% endif %}");
Expand Down
3 changes: 1 addition & 2 deletions tests/conformance_ruby/tags/standard_tag_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ fn test_assign_from_case() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_case_when_or() {
let code = "{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}";
assert_template_result!(" its 1 or 2 or 3 ", code, v!({ "condition": 1 }));
Expand All @@ -323,7 +322,6 @@ fn test_case_when_or() {
}

#[test]
#[should_panic] // liquid-rust#279
fn test_case_when_comma() {
let code =
"{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}";
Expand Down Expand Up @@ -432,6 +430,7 @@ 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 %}");
Expand Down
3 changes: 0 additions & 3 deletions tests/conformance_ruby/tags/statements_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ fn test_zero_lq_or_equal_one() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_zero_lq_or_equal_one_involving_nil() {
let text = " {% if null <= 0 %} true {% else %} false {% endif %} ";
assert_template_result!(" false ", text);
Expand Down Expand Up @@ -110,7 +109,6 @@ fn test_is_not_collection_empty() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_nil() {
let text = " {% if var == nil %} true {% else %} false {% endif %} ";
assert_template_result!(" true ", text, v!({ "var": nil }));
Expand All @@ -120,7 +118,6 @@ fn test_nil() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_not_nil() {
let text = " {% if var != nil %} true {% else %} false {% endif %} ";
assert_template_result!(" true ", text, v!({"var": 1}));
Expand Down
1 change: 0 additions & 1 deletion tests/conformance_ruby/variable_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ fn test_false_renders_as_false() {
}

#[test]
#[should_panic] // liquid-rust#223
fn test_nil_renders_as_empty_string() {
assert_template_result!(r#""#, r#"{{ nil }}"#);

Expand Down

0 comments on commit 7d3b0e5

Please sign in to comment.