Skip to content

Commit fd186dc

Browse files
committed
Fixup cycle rigid parsing to be backwards compatible
1 parent 26f092b commit fd186dc

File tree

2 files changed

+87
-33
lines changed

2 files changed

+87
-33
lines changed

lib/liquid/tags/cycle.rb

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,27 @@ def render_to_output_buffer(context, output)
5858
def rigid_parse(markup)
5959
p = @parse_context.new_parser(markup)
6060

61-
if p.look(:id) && p.look(:colon, 1)
62-
@name = p.consume(:id)
63-
@is_named = true
64-
p.consume(:colon)
65-
end
66-
6761
@variables = []
6862

6963
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
7064

71-
while (var = safe_parse_expression(p))
72-
@variables << var
73-
break unless p.consume?(:comma)
65+
first_expression = safe_parse_expression(p)
66+
if p.look(:colon)
67+
# cycle name: expr1, expr2, ...
68+
@name = first_expression
69+
@is_named = true
70+
p.consume(:colon)
71+
# After the colon, parse the first variable (required for named cycles)
72+
@variables << maybe_dup_lookup(safe_parse_expression(p))
73+
else
74+
# cycle expr1, expr2, ...
75+
@variables << maybe_dup_lookup(first_expression)
76+
end
77+
78+
# Parse remaining comma-separated expressions
79+
while p.consume?(:comma)
80+
break if p.look(:end_of_string)
81+
@variables << maybe_dup_lookup(safe_parse_expression(p))
7482
end
7583

7684
p.consume(:end_of_string)
@@ -106,14 +114,22 @@ def variables_from_string(markup)
106114
var =~ /\s*(#{QuotedFragment})\s*/o
107115
next unless Regexp.last_match(1)
108116

109-
# Expression Parser returns cached objects, and we need to dup them to
110-
# start the cycle over for each new cycle call.
111-
# Liquid-C does not have a cache, so we don't need to dup the object.
112117
var = parse_expression(Regexp.last_match(1))
113-
var.is_a?(VariableLookup) ? var.dup : var
118+
maybe_dup_lookup(var)
114119
end.compact
115120
end
116121

122+
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
123+
# we make it so that the @variables.to_s produces different strings for cycles
124+
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
125+
# This makes it so {% cycle a, b %} and {% cycle a, b %} have independent counters even if a and b share value.
126+
# This is not true for literal values, {% cycle "a", "b" %} and {% cycle "a", "b" %} share the same counter.
127+
# I was really scratching my head about this one, but migrating away from this would be more headache
128+
# than it's worth. So we're keeping this quirk for now.
129+
def maybe_dup_lookup(var)
130+
var.is_a?(VariableLookup) ? var.dup : var
131+
end
132+
117133
class ParseTreeVisitor < Liquid::ParseTreeVisitor
118134
def children
119135
Array(@node.variables)

test/integration/tags/cycle_tag_test.rb

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,11 @@
33
require 'test_helper'
44

55
class CycleTagTest < Minitest::Test
6-
def test_simple_cycle
7-
template = <<~LIQUID
8-
{%- cycle '1', '2', '3' -%}
9-
{%- cycle '1', '2', '3' -%}
10-
{%- cycle '1', '2', '3' -%}
11-
LIQUID
12-
13-
assert_template_result("123", template)
14-
end
156

167
def test_simple_cycle_inside_for_loop
178
template = <<~LIQUID
189
{%- for i in (1..3) -%}
19-
{% cycle '1', '2', '3' %}
10+
{%- cycle '1', '2', '3' -%}
2011
{%- endfor -%}
2112
LIQUID
2213

@@ -36,14 +27,61 @@ def test_cycle_with_variables_inside_for_loop
3627
assert_template_result("123", template)
3728
end
3829

39-
def test_cycle_tag_always_resets_cycle
30+
def test_cycle_named_groups_string
31+
template = <<~LIQUID
32+
{%- for i in (1..3) -%}
33+
{%- cycle 'placeholder1': 1, 2, 3 -%}
34+
{%- cycle 'placeholder2': 1, 2, 3 -%}
35+
{%- endfor -%}
36+
LIQUID
37+
38+
assert_template_result("112233", template)
39+
end
40+
41+
def test_cycle_named_groups_vlookup
42+
template = <<~LIQUID
43+
{%- assign placeholder1 = 'placeholder1' -%}
44+
{%- assign placeholder2 = 'placeholder2' -%}
45+
{%- for i in (1..3) -%}
46+
{%- cycle placeholder1: 1, 2, 3 -%}
47+
{%- cycle placeholder2: 1, 2, 3 -%}
48+
{%- endfor -%}
49+
LIQUID
50+
51+
assert_template_result("112233", template)
52+
end
53+
54+
def test_unnamed_cycle_have_independent_counters_when_used_with_lookups
4055
template = <<~LIQUID
4156
{%- assign a = "1" -%}
42-
{%- cycle a, "2" -%}
43-
{%- cycle a, "2" -%}
57+
{%- for i in (1..3) -%}
58+
{%- cycle a, "2" -%}
59+
{%- cycle a, "2" -%}
60+
{%- endfor -%}
61+
LIQUID
62+
63+
assert_template_result("112211", template)
64+
end
65+
66+
def test_unnamed_cycle_dependent_counter_when_used_with_literal_values
67+
template = <<~LIQUID
68+
{%- cycle "1", "2" -%}
69+
{%- cycle "1", "2" -%}
70+
{%- cycle "1", "2" -%}
4471
LIQUID
4572

46-
assert_template_result("11", template)
73+
assert_template_result("121", template)
74+
end
75+
76+
def test_optional_trailing_comma
77+
template = <<~LIQUID
78+
{%- cycle "1", "2", -%}
79+
{%- cycle "1", "2", -%}
80+
{%- cycle "1", "2", -%}
81+
{%- cycle "1", -%}
82+
LIQUID
83+
84+
assert_template_result("1211", template)
4785
end
4886

4987
def test_cycle_tag_without_arguments
@@ -56,19 +94,19 @@ def test_cycle_tag_without_arguments
5694

5795
def test_cycle_tag_with_error_mode
5896
# QuotedFragment is more permissive than what Parser#expression allows.
59-
temlate1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
60-
temlate2 = "{% cycle .5: 'a', 'b' %}"
97+
template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
98+
template2 = "{% cycle .5: 'a', 'b' %}"
6199

62100
[:lax, :strict].each do |mode|
63101
with_error_mode(mode) do
64-
assert_template_result("b", temlate1)
65-
assert_template_result("a", temlate2)
102+
assert_template_result("b", template1)
103+
assert_template_result("a", template2)
66104
end
67105
end
68106

69107
with_error_mode(:rigid) do
70-
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(temlate1) }
71-
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(temlate2) }
108+
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
109+
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
72110

73111
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
74112

0 commit comments

Comments
 (0)