mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-29 15:15:19 -07:00
* Introduce support for literal comparisons (e.g., {{ 'hello' == 'hello' }})
* Evaluate expressions as truthy/falsy to unlock scenarios, such as `<div class="{{ disabled and "modal--disabled" }}">`
* Add additional scenarios to the expression test suite
* Simplify `LogicalExpression`
This commit is contained in:
@@ -26,31 +26,29 @@ module Liquid
|
|||||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||||
INTEGER_REGEX = /\A(-?\d+)\z/
|
INTEGER_REGEX = /\A(-?\d+)\z/
|
||||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||||
|
QUOTED_STRING = /\A#{QuotedString}\z/
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def parse(markup, ss = StringScanner.new(""), cache = nil, logical_expression = false)
|
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||||
return unless markup
|
return unless markup
|
||||||
|
|
||||||
markup = markup.strip # markup can be a frozen string
|
markup = markup.strip # markup can be a frozen string
|
||||||
|
|
||||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
return markup[1..-2] if QUOTED_STRING.match?(markup)
|
||||||
(markup.start_with?("'") && markup.end_with?("'"))
|
|
||||||
return markup[1..-2]
|
return LITERALS[markup] if LITERALS.key?(markup)
|
||||||
elsif LITERALS.key?(markup)
|
|
||||||
return LITERALS[markup]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Cache only exists during parsing
|
# Cache only exists during parsing
|
||||||
if cache
|
if cache
|
||||||
return cache[markup] if cache.key?(markup)
|
return cache[markup] if cache.key?(markup)
|
||||||
|
|
||||||
cache[markup] = inner_parse(markup, ss, cache, logical_expression).freeze
|
cache[markup] = inner_parse(markup, ss, cache).freeze
|
||||||
else
|
else
|
||||||
inner_parse(markup, ss, nil, logical_expression).freeze
|
inner_parse(markup, ss, nil).freeze
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def inner_parse(markup, ss, cache, logical_expression = false)
|
def inner_parse(markup, ss, cache)
|
||||||
return LogicalExpression.parse(markup, ss, cache) if LogicalExpression.logical?(markup)
|
return LogicalExpression.parse(markup, ss, cache) if LogicalExpression.logical?(markup)
|
||||||
return ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup)
|
return ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup)
|
||||||
|
|
||||||
@@ -66,7 +64,7 @@ module Liquid
|
|||||||
if (num = parse_number(markup, ss))
|
if (num = parse_number(markup, ss))
|
||||||
num
|
num
|
||||||
else
|
else
|
||||||
VariableLookup.parse(markup, ss, cache, logical_expression)
|
VariableLookup.parse(markup, ss, cache)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,34 +19,20 @@ module Liquid
|
|||||||
def parse(markup, ss, cache)
|
def parse(markup, ss, cache)
|
||||||
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
|
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
|
||||||
|
|
||||||
last_expr = expressions.pop
|
expression = expressions.pop
|
||||||
|
condition = parse_condition(expression, ss, cache)
|
||||||
condition = if ComparisonExpression.comparison?(last_expr)
|
|
||||||
ComparisonExpression.parse(last_expr, ss, cache)
|
|
||||||
elsif logical?(last_expr)
|
|
||||||
LogicalExpression.parse(last_expr, ss, cache)
|
|
||||||
else
|
|
||||||
Condition.new(Expression.parse(last_expr, ss, cache, true), nil, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
until expressions.empty?
|
until expressions.empty?
|
||||||
operator = expressions.pop.to_s.strip
|
operator = expressions.pop.to_s.strip
|
||||||
|
|
||||||
next unless boolean_operator?(operator)
|
next unless boolean_operator?(operator)
|
||||||
|
|
||||||
expr = expressions.pop.to_s.strip
|
expression = expressions.pop.to_s.strip
|
||||||
|
new_condition = parse_condition(expression, ss, cache)
|
||||||
|
|
||||||
new_condition = if ComparisonExpression.comparison?(expr)
|
case operator
|
||||||
ComparisonExpression.parse(expr, ss, cache)
|
when 'and' then new_condition.and(condition)
|
||||||
elsif logical?(expr)
|
when 'or' then new_condition.or(condition)
|
||||||
LogicalExpression.parse(expr, ss, cache)
|
|
||||||
else
|
|
||||||
Condition.new(Expression.parse(expr, ss, cache, true), nil, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
if operator == 'and'
|
|
||||||
new_condition.and(condition)
|
|
||||||
else # operator == 'or'
|
|
||||||
new_condition.or(condition)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
condition = new_condition
|
condition = new_condition
|
||||||
@@ -54,6 +40,19 @@ module Liquid
|
|||||||
|
|
||||||
condition
|
condition
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_condition(expr, ss, cache)
|
||||||
|
return ComparisonExpression.parse(expr, ss, cache) if comparison?(expr)
|
||||||
|
return LogicalExpression.parse(expr, ss, cache) if logical?(expr)
|
||||||
|
|
||||||
|
Condition.new(Expression.parse(expr, ss, cache), nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def comparison?(...)
|
||||||
|
ComparisonExpression.comparison?(...)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ module Liquid
|
|||||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||||
|
|
||||||
attr_reader :name, :lookups
|
attr_reader :name, :lookups
|
||||||
attr_accessor :logical_expression
|
|
||||||
|
|
||||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
|
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||||
new(markup, string_scanner, cache, logical_expression)
|
new(markup, string_scanner, cache)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
|
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||||
@logical_expression = logical_expression
|
|
||||||
lookups = markup.scan(VariableParser)
|
lookups = markup.scan(VariableParser)
|
||||||
|
|
||||||
name = lookups.shift
|
name = lookups.shift
|
||||||
@@ -47,17 +45,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def evaluate(context)
|
def evaluate(context)
|
||||||
puts "variable_lookup #evaluate #{@name} #{logical_expression?}"
|
|
||||||
name = context.evaluate(@name)
|
name = context.evaluate(@name)
|
||||||
object = context.find_variable(name)
|
object = context.find_variable(name)
|
||||||
|
|
||||||
# When evaluating a logical expression, this variable lookup is part of a chain of conditions
|
|
||||||
# If the variable lookup returns nil, we must use the falsey value of the variable lookup
|
|
||||||
# rather than nil which is reserved for the usecase of rendering nothing.
|
|
||||||
if logical_expression? && object.nil?
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
@lookups.each_index do |i|
|
@lookups.each_index do |i|
|
||||||
key = context.evaluate(@lookups[i])
|
key = context.evaluate(@lookups[i])
|
||||||
|
|
||||||
@@ -99,10 +89,6 @@ module Liquid
|
|||||||
self.class == other.class && state == other.state
|
self.class == other.class && state == other.state
|
||||||
end
|
end
|
||||||
|
|
||||||
def logical_expression?
|
|
||||||
@logical_expression
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def state
|
def state
|
||||||
|
|||||||
@@ -95,10 +95,58 @@ class BooleanUnitTest < Minitest::Test
|
|||||||
assert_equal("true", template.render("media_position" => 2))
|
assert_equal("true", template.render("media_position" => 2))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_equality_operators
|
def test_equality_operators_with_integer_literals
|
||||||
assert_parity("1 == 1", "true")
|
assert_expression("1", "1")
|
||||||
assert_parity("1 != 2", "true")
|
assert_expression("1 == 1", "true")
|
||||||
assert_parity_todo!("'hello' == 'hello'", "true")
|
assert_expression("1 != 1", "false")
|
||||||
|
assert_expression("1 == 2", "false")
|
||||||
|
assert_expression("1 != 2", "true")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_equality_operators_with_stirng_literals
|
||||||
|
assert_expression("'hello'", "hello")
|
||||||
|
assert_expression("'hello' == 'hello'", "true")
|
||||||
|
assert_expression("'hello' != 'hello'", "false")
|
||||||
|
assert_expression("'hello' == 'world'", "false")
|
||||||
|
assert_expression("'hello' != 'world'", "true")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_equality_operators_with_float_literals
|
||||||
|
assert_expression("1.5", "1.5")
|
||||||
|
assert_expression("1.5 == 1.5", "true")
|
||||||
|
assert_expression("1.5 != 1.5", "false")
|
||||||
|
assert_expression("1.5 == 2.5", "false")
|
||||||
|
assert_expression("1.5 != 2.5", "true")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_equality_operators_with_nil_literals
|
||||||
|
assert_expression("nil", "")
|
||||||
|
assert_expression("nil == nil", "true")
|
||||||
|
assert_expression("nil != nil", "false")
|
||||||
|
assert_expression("null == nil", "true")
|
||||||
|
assert_expression("null != nil", "false")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_equality_operators_with_boolean_literals
|
||||||
|
assert_expression("true", "true")
|
||||||
|
assert_expression("false", "false")
|
||||||
|
assert_expression("true == true", "true")
|
||||||
|
assert_expression("true != true", "false")
|
||||||
|
assert_expression("false == false", "true")
|
||||||
|
assert_expression("false != false", "false")
|
||||||
|
assert_expression("true == false", "false")
|
||||||
|
assert_expression("true != false", "true")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_equality_operators_with_empty_literals
|
||||||
|
assert_expression("empty", "")
|
||||||
|
assert_expression("empty == ''", "true")
|
||||||
|
assert_expression("empty == empty", "true")
|
||||||
|
assert_expression("empty != empty", "false")
|
||||||
|
assert_expression("blank == blank", "true")
|
||||||
|
assert_expression("blank != blank", "false")
|
||||||
|
assert_expression("empty == blank", "true")
|
||||||
|
assert_expression("empty != blank", "false")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_nil_renders_as_empty_string
|
def test_nil_renders_as_empty_string
|
||||||
@@ -122,18 +170,41 @@ class BooleanUnitTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_nil_variable_in_and_expression
|
def test_nil_variable_in_and_expression
|
||||||
assert_parity("x and true", "false", { "x" => nil })
|
assert_condition("x and true", "false", { "x" => nil })
|
||||||
assert_parity("true and x", "false", { "x" => nil })
|
assert_condition("true and x", "false", { "x" => nil })
|
||||||
|
|
||||||
|
assert_expression("x and true", "", { "x" => nil })
|
||||||
|
assert_expression("true and x", "", { "x" => nil })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_boolean_variable_in_and_expression
|
def test_boolean_variable_in_and_expression
|
||||||
assert_parity("true and x", "false", { "x" => false })
|
assert_parity("true and x", "false", { "x" => false })
|
||||||
assert_parity("x and true", "false", { "x" => false })
|
assert_parity("x and true", "false", { "x" => false })
|
||||||
|
|
||||||
|
assert_parity("true and x", "true", { "x" => true })
|
||||||
|
assert_parity("x and true", "true", { "x" => true })
|
||||||
|
|
||||||
|
assert_parity("true or x", "true", { "x" => false })
|
||||||
|
assert_parity("x or true", "true", { "x" => false })
|
||||||
|
|
||||||
|
assert_parity("true or x", "true", { "x" => true })
|
||||||
|
assert_parity("x or true", "true", { "x" => true })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_multi_variable_boolean_nil_and_expression
|
def test_multi_variable_boolean_nil_and_expression
|
||||||
assert_parity("x and y", "false", { "x" => nil, "y" => true })
|
assert_condition("x and y", "false", { "x" => nil, "y" => true })
|
||||||
assert_parity("y and x", "false", { "x" => true, "y" => nil })
|
assert_condition("y and x", "false", { "x" => true, "y" => nil })
|
||||||
|
|
||||||
|
assert_expression("x and y", "", { "x" => nil, "y" => true })
|
||||||
|
assert_expression("y and x", "", { "x" => true, "y" => nil })
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multi_truthy_variables_and_expressions
|
||||||
|
assert_condition("x or y", "true", { "x" => nil, "y" => "hello" })
|
||||||
|
assert_condition("y or x", "true", { "x" => "hello", "y" => nil })
|
||||||
|
|
||||||
|
assert_expression("x or y", "hello", { "x" => nil, "y" => "hello" })
|
||||||
|
assert_expression("y or x", "hello", { "x" => "hello", "y" => nil })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_multi_variable_boolean_nil_or_expression
|
def test_multi_variable_boolean_nil_or_expression
|
||||||
|
|||||||
Reference in New Issue
Block a user