Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5782a9e
TDD: Unit tests for new liquid syntax
albchu Mar 4, 2025
b036feb
Removed infix operators from this PR
albchu Mar 4, 2025
f32c0fb
TDD: Improved unit tests in boolean_unit_test.rb
albchu Mar 4, 2025
6d4cffa
Support for simple boolean comparisons and boolean assignments
albchu Mar 5, 2025
14b0d64
Rough support for parenthesis. Also better respect for and/or order p…
albchu Mar 5, 2025
03feea9
Added a lot more boolean unit tests
albchu Mar 5, 2025
9b96769
Introduce support to boolean operators in the lexer
karreiro Mar 5, 2025
ba0bbe3
Update the parser to use the new tokens
karreiro Mar 5, 2025
26ccec1
* Move expression handling from variable.rb to expression.rb
karreiro Mar 5, 2025
b57f4fc
Support usecase where a nil variable value is used in a logical expre…
albchu Mar 5, 2025
b31f24b
More boolean unit tests and enabled more existing parity cases
albchu Mar 5, 2025
9e6b628
* Introduce support for literal comparisons (e.g., `{{ 'hello' == 'he…
karreiro Mar 6, 2025
263a73b
Remove 'assert_parity_todo!'
karreiro Mar 6, 2025
51a05c2
Blank parity
karreiro Mar 7, 2025
b5c3d3f
Introduced debugging gems
albchu Mar 11, 2025
08d36b0
Fixed ComparisonExpression.parse with MethodLiterals
albchu Mar 11, 2025
430794d
Added test for operator reading bug
albchu Mar 11, 2025
4b57b2b
Reintroduced broken conditional operators behaviour present in liquid…
albchu Mar 12, 2025
484f016
Added failing unit test that passes in main
albchu Mar 13, 2025
e6e8221
Fixed lax parsing test case
albchu Mar 13, 2025
6148604
Added another failing unit test for behavior in main
albchu Mar 13, 2025
1ae2ff1
Added more non-parity unit tests
albchu Mar 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ group :development do
end

group :test do
gem 'ruby-lsp'
gem 'debug'
gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false
Expand Down
2 changes: 2 additions & 0 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ module Liquid
require 'liquid/range_lookup'
require 'liquid/resource_limits'
require 'liquid/expression'
require 'liquid/expression/comparison_expression'
require 'liquid/expression/logical_expression'
require 'liquid/template'
require 'liquid/condition'
require 'liquid/utils'
Expand Down
12 changes: 10 additions & 2 deletions lib/liquid/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def self.parse_expression(parse_context, markup)
@@method_literals[markup] || parse_context.parse_expression(markup)
end

def self.parse(markup, ss, cache)
@@method_literals[markup] || Expression.parse(markup, ss, cache)
end

attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right

Expand Down Expand Up @@ -112,19 +116,23 @@ def inspect
private

def equal_variables(left, right)
if left.is_a?(MethodLiteral) && right.is_a?(MethodLiteral)
return left.to_s == right.to_s
end

if left.is_a?(MethodLiteral)
if right.respond_to?(left.method_name)
return right.send(left.method_name)
else
return nil
return left.to_s == right
end
end

if right.is_a?(MethodLiteral)
if left.respond_to?(right.method_name)
return left.send(right.method_name)
else
return nil
return right.to_s == left
end
end

Expand Down
13 changes: 7 additions & 6 deletions lib/liquid/expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,17 @@ class Expression
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)
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
Expand All @@ -51,6 +49,9 @@ def parse(markup, ss = StringScanner.new(""), cache = nil)
end

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)

if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Expand Down
31 changes: 31 additions & 0 deletions lib/liquid/expression/comparison_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Liquid
class Expression
class ComparisonExpression
# We can improve the resiliency of lax parsing by not expecting whitespace
# surrounding the operator (ie \s+ => \s*).
# However this is not in parity with existing lax parsing behavior.
COMPARISON_REGEX = /\A\s*(.+?)\s+(==|!=|<>|<=|>=|<|>|contains)\s+(.+)\s*\z/

class << self
def comparison?(markup)
markup.match(COMPARISON_REGEX)
end

def parse(markup, ss, cache)
match = comparison?(markup)

if match
left = Condition.parse(match[1].strip, ss, cache)
operator = match[2].strip
right = Condition.parse(match[3].strip, ss, cache)
return Condition.new(left, operator, right)
end

Condition.new(parse(markup, ss, cache), nil, nil)
end
end
end
end
end
59 changes: 59 additions & 0 deletions lib/liquid/expression/logical_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Liquid
class Expression
class LogicalExpression
LOGICAL_REGEX = /\A\s*(.+?)\s+(and|or)\s+(.+)\s*\z/i
EXPRESSIONS_AND_OPERATORS = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = ['and', 'or'].freeze

class << self
def logical?(markup)
markup.match(LOGICAL_REGEX)
end

def boolean_operator?(markup)
BOOLEAN_OPERATORS.include?(markup)
end

def parse(markup, ss, cache)
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)

expression = expressions.pop
condition = parse_condition(expression, ss, cache)

until expressions.empty?
operator = expressions.pop.to_s.strip

next unless boolean_operator?(operator)

expression = expressions.pop.to_s.strip
new_condition = parse_condition(expression, ss, cache)

case operator
when 'and' then new_condition.and(condition)
when 'or' then new_condition.or(condition)
end

condition = new_condition
end

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
end
6 changes: 6 additions & 0 deletions lib/liquid/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Lexer
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
BOOLEAN_AND = [:boolean_operator, "and"].freeze
BOOLEAN_OR = [:boolean_operator, "or"].freeze
DASH = [:dash, "-"].freeze
DOT = [:dot, "."].freeze
DOTDOT = [:dotdot, ".."].freeze
Expand Down Expand Up @@ -151,6 +153,10 @@ def tokenize(ss)
# Special case for "contains"
output << if type == :id && t == "contains" && output.last&.first != :dot
COMPARISON_CONTAINS
elsif type == :id && t == "and" && output.last&.first != :dot
BOOLEAN_AND
elsif type == :id && t == "or" && output.last&.first != :dot
BOOLEAN_OR
else
[type, t]
end
Expand Down
17 changes: 16 additions & 1 deletion lib/liquid/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def look(type, ahead = 0)

def expression
token = @tokens[@p]
case token[0]
expr = case token[0]
when :id
str = consume
str << variable_lookups
Expand All @@ -69,6 +69,21 @@ def expression
else
raise SyntaxError, "#{token} is not a valid expression"
end
if look(:comparison)
operator = consume(:comparison)
left = expr
right = expression

"#{left} #{operator} #{right}"
elsif look(:boolean_operator)
operator = consume(:boolean_operator)
left = expr
right = expression

"#{left} #{operator} #{right}"
else
expr
end
end

def argument
Expand Down
Loading
Loading