* 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:
Guilherme Carreiro
2025-03-10 21:20:44 -06:00
committed by Albert Chu
parent b31f24bdf0
commit 9e6b628a68
4 changed files with 112 additions and 58 deletions
+9 -11
View File
@@ -26,31 +26,29 @@ module Liquid
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
QUOTED_STRING = /\A#{QuotedString}\z/
class << self
def parse(markup, ss = StringScanner.new(""), cache = nil, logical_expression = false)
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
markup = markup.strip # markup can be a frozen string
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
elsif LITERALS.key?(markup)
return LITERALS[markup]
end
return markup[1..-2] if QUOTED_STRING.match?(markup)
return LITERALS[markup] if LITERALS.key?(markup)
# Cache only exists during parsing
if cache
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
inner_parse(markup, ss, nil, logical_expression).freeze
inner_parse(markup, ss, nil).freeze
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 ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup)
@@ -66,7 +64,7 @@ module Liquid
if (num = parse_number(markup, ss))
num
else
VariableLookup.parse(markup, ss, cache, logical_expression)
VariableLookup.parse(markup, ss, cache)
end
end
+21 -22
View File
@@ -19,34 +19,20 @@ module Liquid
def parse(markup, ss, cache)
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
last_expr = expressions.pop
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
expression = expressions.pop
condition = parse_condition(expression, ss, cache)
until expressions.empty?
operator = expressions.pop.to_s.strip
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)
ComparisonExpression.parse(expr, ss, cache)
elsif logical?(expr)
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)
case operator
when 'and' then new_condition.and(condition)
when 'or' then new_condition.or(condition)
end
condition = new_condition
@@ -54,6 +40,19 @@ module Liquid
condition
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
+3 -17
View File
@@ -5,14 +5,12 @@ module Liquid
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
attr_accessor :logical_expression
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
new(markup, string_scanner, cache, logical_expression)
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup, string_scanner, cache)
end
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
@logical_expression = logical_expression
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
lookups = markup.scan(VariableParser)
name = lookups.shift
@@ -47,17 +45,9 @@ module Liquid
end
def evaluate(context)
puts "variable_lookup #evaluate #{@name} #{logical_expression?}"
name = context.evaluate(@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|
key = context.evaluate(@lookups[i])
@@ -99,10 +89,6 @@ module Liquid
self.class == other.class && state == other.state
end
def logical_expression?
@logical_expression
end
protected
def state