Skip to content

Commit 1308b97

Browse files
committed
Stricter 1:1 refactor of strict_parse for Variable
1 parent 533396b commit 1308b97

File tree

3 files changed

+101
-17
lines changed

3 files changed

+101
-17
lines changed

lib/liquid/parser_switching.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ module ParserSwitching
55
# Do not use this.
66
#
77
# It's basically doing the same thing the {#parse_with_selected_parser},
8-
# except this will use the strict parser, instead of the rigid parser.
8+
# except this will try the strict parser regardless of the error mode,
9+
# and fall back to the lax parser if the error mode is lax or warn.
910
#
1011
# @deprecated Use {#parse_with_selected_parser} instead.
1112
def strict_parse_with_error_mode_fallback(markup)
12-
strict_parse_with_error_context(markup)
13+
case parse_context.error_mode
14+
when :rigid
15+
rigid_parse_with_error_context(markup)
16+
else
17+
strict_parse_with_error_context(markup)
18+
end
1319
rescue SyntaxError => e
1420
case parse_context.error_mode
1521
when :rigid

lib/liquid/variable.rb

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def lax_parse(markup)
5454
next unless f =~ /\w+/
5555
filtername = Regexp.last_match(0)
5656
filterargs = f.scan(FilterArgsRegex).flatten
57-
@filters << parse_filter_expressions(filtername, filterargs)
57+
@filters << lax_parse_filter_expressions(filtername, filterargs)
5858
end
5959
end
6060
end
@@ -66,14 +66,14 @@ def strict_parse(markup)
6666
return if p.look(:end_of_string)
6767

6868
@name = parse_context.safe_parse_expression(p)
69-
while p.consume?(:pipe)
70-
filtername = p.consume(:id)
71-
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
72-
@filters << parse_filter_expressions(filtername, filterargs, safe: true)
73-
end
69+
@filters << strict_parse_filter_expressions(p) while p.consume?(:pipe)
7470
p.consume(:end_of_string)
7571
end
7672

73+
def rigid_parse(markup)
74+
strict_parse(markup)
75+
end
76+
7777
def parse_filterargs(p)
7878
# first argument
7979
filterargs = [p.argument]
@@ -122,22 +122,62 @@ def disabled_tags
122122

123123
private
124124

125-
def parse_filter_expressions(filter_name, unparsed_args, safe: false)
125+
def lax_parse_filter_expressions(filter_name, unparsed_args)
126126
filter_args = []
127127
keyword_args = nil
128128
unparsed_args.each do |a|
129-
if (matches = a.match(JustTagAttributes)) # we'll need to fix this
129+
if (matches = a.match(JustTagAttributes))
130130
keyword_args ||= {}
131-
keyword_args[matches[1]] = parse_context.parse_expression(matches[2], safe: false)
131+
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
132132
else
133-
filter_args << parse_context.parse_expression(a, safe: safe)
133+
filter_args << parse_context.parse_expression(a)
134134
end
135135
end
136136
result = [filter_name, filter_args]
137137
result << keyword_args if keyword_args
138138
result
139139
end
140140

141+
# Surprisingly, positional and keyword arguments can be mixed.
142+
#
143+
# filter = filtername [":" filterargs?]
144+
# filterargs = argument ("," argument)*
145+
# argument = (positional_argument | keyword_argument)
146+
# positional_argument = expression
147+
# keyword_argument = id ":" expression
148+
def strict_parse_filter_expressions(p)
149+
filtername = p.consume(:id)
150+
filter_args = []
151+
keyword_args = {}
152+
153+
if p.consume?(:colon)
154+
# Parse first argument (no leading comma)
155+
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
156+
157+
# Parse remaining arguments (with leading commas) and optional trailing comma
158+
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
159+
end
160+
161+
result = [filtername, filter_args]
162+
result << keyword_args unless keyword_args.empty?
163+
result
164+
end
165+
166+
def argument(p, positional_arguments, keyword_arguments)
167+
if p.look(:id) && p.look(:colon, 1)
168+
key = p.consume(:id)
169+
p.consume(:colon)
170+
value = parse_context.safe_parse_expression(p)
171+
keyword_arguments[key] = value
172+
else
173+
positional_arguments << parse_context.safe_parse_expression(p)
174+
end
175+
end
176+
177+
def end_of_arguments?(p)
178+
p.look(:pipe) || p.look(:end_of_string)
179+
end
180+
141181
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
142182
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
143183
if filter_kwargs

test/unit/variable_unit_test.rb

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,52 @@ def test_lax_filter_argument_parsing
135135
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
136136
assert_equal(VariableLookup.new('number_of_comments'), var.name)
137137
assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
138+
139+
# missing does not throws error
140+
create_variable(%(n | f1: ,), error_mode: :lax)
141+
create_variable(%(n | f1: ,| f2), error_mode: :lax)
142+
143+
# arg does not require colon, but ignores args :O, also ignores first kwarg since it splits on ':'
144+
var = create_variable(%(n | f1 1 | f2 k1: v1), error_mode: :lax)
145+
assert_equal([['f1', []], ['f2', [VariableLookup.new('v1')]]], var.filters)
146+
147+
# positional and kwargs parsing
148+
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2), error_mode: :lax)
149+
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
150+
151+
# positional and kwargs intermixed (pos1, key1: val1, pos2)
152+
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"), error_mode: :lax)
153+
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
138154
end
139155

140156
def test_strict_filter_argument_parsing
141-
with_error_mode(:strict) do
142-
assert_raises(SyntaxError) do
143-
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
144-
end
145-
end
157+
# optional colon
158+
var = create_variable(%(n | f1 | f2:), error_mode: :strict)
159+
assert_equal([['f1', []], ['f2', []]], var.filters)
160+
161+
# missing argument throws error
162+
assert_raises(SyntaxError) { create_variable(%(n | f1: ,), error_mode: :strict) }
163+
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2), error_mode: :strict) }
164+
165+
# arg requires colon
166+
assert_raises(SyntaxError) { create_variable(%(n | f1 1), error_mode: :strict) }
167+
168+
# trailing comma doesn't throw
169+
create_variable(%(n | f1: 1, 2, 3, | f2:), error_mode: :strict)
170+
171+
# missing comma throws error
172+
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3), error_mode: :strict) }
173+
174+
# positional and kwargs parsing
175+
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2), error_mode: :strict)
176+
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
177+
178+
# positional and kwargs intermixed (pos1, key1: val1, pos2)
179+
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"), error_mode: :strict)
180+
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
181+
182+
# string key throws
183+
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments'), error_mode: :strict) }
146184
end
147185

148186
def test_output_raw_source_of_variable

0 commit comments

Comments
 (0)