Skip to content

Commit fe49f49

Browse files
committedMar 6, 2025
* 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
1 parent f154ac1 commit fe49f49

File tree

4 files changed

+112
-58
lines changed

4 files changed

+112
-58
lines changed
 

‎lib/liquid/expression.rb

+9-11
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,29 @@ class Expression
2626
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
2727
INTEGER_REGEX = /\A(-?\d+)\z/
2828
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
29+
QUOTED_STRING = /\A#{QuotedString}\z/
2930

3031
class << self
31-
def parse(markup, ss = StringScanner.new(""), cache = nil, logical_expression = false)
32+
def parse(markup, ss = StringScanner.new(""), cache = nil)
3233
return unless markup
3334

3435
markup = markup.strip # markup can be a frozen string
3536

36-
if (markup.start_with?('"') && markup.end_with?('"')) ||
37-
(markup.start_with?("'") && markup.end_with?("'"))
38-
return markup[1..-2]
39-
elsif LITERALS.key?(markup)
40-
return LITERALS[markup]
41-
end
37+
return markup[1..-2] if QUOTED_STRING.match?(markup)
38+
39+
return LITERALS[markup] if LITERALS.key?(markup)
4240

4341
# Cache only exists during parsing
4442
if cache
4543
return cache[markup] if cache.key?(markup)
4644

47-
cache[markup] = inner_parse(markup, ss, cache, logical_expression).freeze
45+
cache[markup] = inner_parse(markup, ss, cache).freeze
4846
else
49-
inner_parse(markup, ss, nil, logical_expression).freeze
47+
inner_parse(markup, ss, nil).freeze
5048
end
5149
end
5250

53-
def inner_parse(markup, ss, cache, logical_expression = false)
51+
def inner_parse(markup, ss, cache)
5452
return LogicalExpression.parse(markup, ss, cache) if LogicalExpression.logical?(markup)
5553
return ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup)
5654

@@ -66,7 +64,7 @@ def inner_parse(markup, ss, cache, logical_expression = false)
6664
if (num = parse_number(markup, ss))
6765
num
6866
else
69-
VariableLookup.parse(markup, ss, cache, logical_expression)
67+
VariableLookup.parse(markup, ss, cache)
7068
end
7169
end
7270

‎lib/liquid/expression/logical_expression.rb

+21-22
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,40 @@ def boolean_operator?(markup)
1919
def parse(markup, ss, cache)
2020
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
2121

22-
last_expr = expressions.pop
23-
24-
condition = if ComparisonExpression.comparison?(last_expr)
25-
ComparisonExpression.parse(last_expr, ss, cache)
26-
elsif logical?(last_expr)
27-
LogicalExpression.parse(last_expr, ss, cache)
28-
else
29-
Condition.new(Expression.parse(last_expr, ss, cache, true), nil, nil)
30-
end
22+
expression = expressions.pop
23+
condition = parse_condition(expression, ss, cache)
3124

3225
until expressions.empty?
3326
operator = expressions.pop.to_s.strip
27+
3428
next unless boolean_operator?(operator)
3529

36-
expr = expressions.pop.to_s.strip
30+
expression = expressions.pop.to_s.strip
31+
new_condition = parse_condition(expression, ss, cache)
3732

38-
new_condition = if ComparisonExpression.comparison?(expr)
39-
ComparisonExpression.parse(expr, ss, cache)
40-
elsif logical?(expr)
41-
LogicalExpression.parse(expr, ss, cache)
42-
else
43-
Condition.new(Expression.parse(expr, ss, cache, true), nil, nil)
44-
end
45-
46-
if operator == 'and'
47-
new_condition.and(condition)
48-
else # operator == 'or'
49-
new_condition.or(condition)
33+
case operator
34+
when 'and' then new_condition.and(condition)
35+
when 'or' then new_condition.or(condition)
5036
end
5137

5238
condition = new_condition
5339
end
5440

5541
condition
5642
end
43+
44+
private
45+
46+
def parse_condition(expr, ss, cache)
47+
return ComparisonExpression.parse(expr, ss, cache) if comparison?(expr)
48+
return LogicalExpression.parse(expr, ss, cache) if logical?(expr)
49+
50+
Condition.new(Expression.parse(expr, ss, cache), nil, nil)
51+
end
52+
53+
def comparison?(...)
54+
ComparisonExpression.comparison?(...)
55+
end
5756
end
5857
end
5958
end

‎lib/liquid/variable_lookup.rb

+3-17
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@ class VariableLookup
55
COMMAND_METHODS = ['size', 'first', 'last'].freeze
66

77
attr_reader :name, :lookups
8-
attr_accessor :logical_expression
98

10-
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
11-
new(markup, string_scanner, cache, logical_expression)
9+
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
10+
new(markup, string_scanner, cache)
1211
end
1312

14-
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
15-
@logical_expression = logical_expression
13+
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
1614
lookups = markup.scan(VariableParser)
1715

1816
name = lookups.shift
@@ -47,17 +45,9 @@ def lookup_command?(lookup_index)
4745
end
4846

4947
def evaluate(context)
50-
puts "variable_lookup #evaluate #{@name} #{logical_expression?}"
5148
name = context.evaluate(@name)
5249
object = context.find_variable(name)
5350

54-
# When evaluating a logical expression, this variable lookup is part of a chain of conditions
55-
# If the variable lookup returns nil, we must use the falsey value of the variable lookup
56-
# rather than nil which is reserved for the usecase of rendering nothing.
57-
if logical_expression? && object.nil?
58-
return false
59-
end
60-
6151
@lookups.each_index do |i|
6252
key = context.evaluate(@lookups[i])
6353

@@ -99,10 +89,6 @@ def ==(other)
9989
self.class == other.class && state == other.state
10090
end
10191

102-
def logical_expression?
103-
@logical_expression
104-
end
105-
10692
protected
10793

10894
def state

‎test/unit/boolean_unit_test.rb

+79-8
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,58 @@ def test_boolean_assignment_shorthand
9595
assert_equal("true", template.render("media_position" => 2))
9696
end
9797

98-
def test_equality_operators
99-
assert_parity("1 == 1", "true")
100-
assert_parity("1 != 2", "true")
101-
assert_parity_todo!("'hello' == 'hello'", "true")
98+
def test_equality_operators_with_integer_literals
99+
assert_expression("1", "1")
100+
assert_expression("1 == 1", "true")
101+
assert_expression("1 != 1", "false")
102+
assert_expression("1 == 2", "false")
103+
assert_expression("1 != 2", "true")
104+
end
105+
106+
def test_equality_operators_with_stirng_literals
107+
assert_expression("'hello'", "hello")
108+
assert_expression("'hello' == 'hello'", "true")
109+
assert_expression("'hello' != 'hello'", "false")
110+
assert_expression("'hello' == 'world'", "false")
111+
assert_expression("'hello' != 'world'", "true")
112+
end
113+
114+
def test_equality_operators_with_float_literals
115+
assert_expression("1.5", "1.5")
116+
assert_expression("1.5 == 1.5", "true")
117+
assert_expression("1.5 != 1.5", "false")
118+
assert_expression("1.5 == 2.5", "false")
119+
assert_expression("1.5 != 2.5", "true")
120+
end
121+
122+
def test_equality_operators_with_nil_literals
123+
assert_expression("nil", "")
124+
assert_expression("nil == nil", "true")
125+
assert_expression("nil != nil", "false")
126+
assert_expression("null == nil", "true")
127+
assert_expression("null != nil", "false")
128+
end
129+
130+
def test_equality_operators_with_boolean_literals
131+
assert_expression("true", "true")
132+
assert_expression("false", "false")
133+
assert_expression("true == true", "true")
134+
assert_expression("true != true", "false")
135+
assert_expression("false == false", "true")
136+
assert_expression("false != false", "false")
137+
assert_expression("true == false", "false")
138+
assert_expression("true != false", "true")
139+
end
140+
141+
def test_equality_operators_with_empty_literals
142+
assert_expression("empty", "")
143+
assert_expression("empty == ''", "true")
144+
assert_expression("empty == empty", "true")
145+
assert_expression("empty != empty", "false")
146+
assert_expression("blank == blank", "true")
147+
assert_expression("blank != blank", "false")
148+
assert_expression("empty == blank", "true")
149+
assert_expression("empty != blank", "false")
102150
end
103151

104152
def test_nil_renders_as_empty_string
@@ -122,18 +170,41 @@ def test_if_with_variables
122170
end
123171

124172
def test_nil_variable_in_and_expression
125-
assert_parity("x and true", "false", { "x" => nil })
126-
assert_parity("true and x", "false", { "x" => nil })
173+
assert_condition("x and true", "false", { "x" => nil })
174+
assert_condition("true and x", "false", { "x" => nil })
175+
176+
assert_expression("x and true", "", { "x" => nil })
177+
assert_expression("true and x", "", { "x" => nil })
127178
end
128179

129180
def test_boolean_variable_in_and_expression
130181
assert_parity("true and x", "false", { "x" => false })
131182
assert_parity("x and true", "false", { "x" => false })
183+
184+
assert_parity("true and x", "true", { "x" => true })
185+
assert_parity("x and true", "true", { "x" => true })
186+
187+
assert_parity("true or x", "true", { "x" => false })
188+
assert_parity("x or true", "true", { "x" => false })
189+
190+
assert_parity("true or x", "true", { "x" => true })
191+
assert_parity("x or true", "true", { "x" => true })
132192
end
133193

134194
def test_multi_variable_boolean_nil_and_expression
135-
assert_parity("x and y", "false", { "x" => nil, "y" => true })
136-
assert_parity("y and x", "false", { "x" => true, "y" => nil })
195+
assert_condition("x and y", "false", { "x" => nil, "y" => true })
196+
assert_condition("y and x", "false", { "x" => true, "y" => nil })
197+
198+
assert_expression("x and y", "", { "x" => nil, "y" => true })
199+
assert_expression("y and x", "", { "x" => true, "y" => nil })
200+
end
201+
202+
def test_multi_truthy_variables_and_expressions
203+
assert_condition("x or y", "true", { "x" => nil, "y" => "hello" })
204+
assert_condition("y or x", "true", { "x" => "hello", "y" => nil })
205+
206+
assert_expression("x or y", "hello", { "x" => nil, "y" => "hello" })
207+
assert_expression("y or x", "hello", { "x" => "hello", "y" => nil })
137208
end
138209

139210
def test_multi_variable_boolean_nil_or_expression

0 commit comments

Comments
 (0)
Failed to load comments.