Skip to content

Commit 376d849

Browse files
committed
Add rigid parser to tablerow tag
1 parent fd81ac1 commit 376d849

File tree

4 files changed

+320
-6
lines changed

4 files changed

+320
-6
lines changed

lib/liquid/locales/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
invalid_template_encoding: "Invalid template encoding"
2121
render: "Syntax error in tag 'render' - Template name must be a quoted string"
2222
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
23+
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
2324
tag_never_closed: "'%{block_name}' tag was never closed"
2425
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
2526
unexpected_else: "%{block_name} tag does not expect 'else' tag"

lib/liquid/tags/table_row.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,49 @@ module Liquid
2525
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
2626
class TableRow < Block
2727
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
28+
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
2829

2930
attr_reader :variable_name, :collection_name, :attributes
3031

3132
def initialize(tag_name, markup, options)
3233
super
34+
parse_with_selected_parser(markup)
35+
end
36+
37+
def rigid_parse(markup)
38+
p = @parse_context.new_parser(markup)
39+
40+
@variable_name = p.consume(:id)
41+
42+
unless p.id?("in")
43+
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
44+
end
45+
46+
@collection_name = safe_parse_expression(p)
47+
48+
# optional comma
49+
p.consume?(:comma)
50+
51+
@attributes = {}
52+
while p.look(:id)
53+
key = p.consume
54+
unless ALLOWED_ATTRIBUTES.include?(key)
55+
raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
56+
end
57+
58+
p.consume(:colon)
59+
@attributes[key] = safe_parse_expression(p)
60+
p.consume?(:comma) # optional comma
61+
end
62+
63+
p.consume(:end_of_string)
64+
end
65+
66+
def strict_parse(markup)
67+
lax_parse(markup)
68+
end
69+
70+
def lax_parse(markup)
3371
if markup =~ Syntax
3472
@variable_name = Regexp.last_match(1)
3573
@collection_name = parse_expression(Regexp.last_match(2))

test/integration/tags/table_row_test.rb

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,261 @@ def test_table_row_does_not_leak_interrupts
255255
template,
256256
)
257257
end
258+
259+
def test_tablerow_with_cols_attribute_in_rigid_mode
260+
template = <<~LIQUID.chomp
261+
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
262+
LIQUID
263+
264+
expected = <<~OUTPUT
265+
<tr class="row1">
266+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
267+
<tr class="row2"><td class="col1">4</td><td class="col2">5</td><td class="col3">6</td></tr>
268+
OUTPUT
269+
270+
result = Template.parse(template, environment: rigid_environment).render
271+
assert_equal(expected, result)
272+
end
273+
274+
def test_tablerow_with_limit_attribute_in_rigid_mode
275+
template = <<~LIQUID.chomp
276+
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
277+
LIQUID
278+
279+
expected = <<~OUTPUT
280+
<tr class="row1">
281+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
282+
OUTPUT
283+
284+
result = Template.parse(template, environment: rigid_environment).render
285+
assert_equal(expected, result)
286+
end
287+
288+
def test_tablerow_with_offset_attribute_in_rigid_mode
289+
template = <<~LIQUID.chomp
290+
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
291+
LIQUID
292+
293+
expected = <<~OUTPUT
294+
<tr class="row1">
295+
<td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
296+
OUTPUT
297+
298+
result = Template.parse(template, environment: rigid_environment).render
299+
assert_equal(expected, result)
300+
end
301+
302+
def test_tablerow_with_range_attribute_in_rigid_mode
303+
template = <<~LIQUID.chomp
304+
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
305+
LIQUID
306+
307+
expected = <<~OUTPUT
308+
<tr class="row1">
309+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
310+
OUTPUT
311+
312+
result = Template.parse(template, environment: rigid_environment).render
313+
assert_equal(expected, result)
314+
end
315+
316+
def test_tablerow_with_multiple_attributes_in_rigid_mode
317+
template = <<~LIQUID.chomp
318+
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
319+
LIQUID
320+
321+
expected = <<~OUTPUT
322+
<tr class="row1">
323+
<td class="col1">2</td><td class="col2">3</td></tr>
324+
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
325+
OUTPUT
326+
327+
result = Template.parse(template, environment: rigid_environment).render
328+
assert_equal(expected, result)
329+
end
330+
331+
def test_tablerow_with_variable_collection_in_rigid_mode
332+
template = <<~LIQUID.chomp
333+
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
334+
LIQUID
335+
336+
expected = <<~OUTPUT
337+
<tr class="row1">
338+
<td class="col1">1</td><td class="col2">2</td></tr>
339+
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
340+
OUTPUT
341+
342+
result = Template.parse(template, environment: rigid_environment).render('numbers' => [1, 2, 3, 4])
343+
assert_equal(expected, result)
344+
end
345+
346+
def test_tablerow_with_dotted_access_in_rigid_mode
347+
template = <<~LIQUID.chomp
348+
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
349+
LIQUID
350+
351+
expected = <<~OUTPUT
352+
<tr class="row1">
353+
<td class="col1">1</td><td class="col2">2</td></tr>
354+
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
355+
OUTPUT
356+
357+
result = Template.parse(template, environment: rigid_environment).render('obj' => { 'numbers' => [1, 2, 3, 4] })
358+
assert_equal(expected, result)
359+
end
360+
361+
def test_tablerow_with_bracketed_access_in_rigid_mode
362+
template = <<~LIQUID.chomp
363+
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
364+
LIQUID
365+
366+
expected = <<~OUTPUT
367+
<tr class="row1">
368+
<td class="col1">10</td><td class="col2">20</td></tr>
369+
OUTPUT
370+
371+
result = Template.parse(template, environment: rigid_environment).render('obj' => { 'numbers' => [10, 20] })
372+
assert_equal(expected, result)
373+
end
374+
375+
def test_tablerow_without_attributes_in_rigid_mode
376+
template = <<~LIQUID.chomp
377+
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
378+
LIQUID
379+
380+
expected = <<~OUTPUT
381+
<tr class="row1">
382+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
383+
OUTPUT
384+
385+
result = Template.parse(template, environment: rigid_environment).render
386+
assert_equal(expected, result)
387+
end
388+
389+
def test_tablerow_with_trailing_comma_in_rigid_mode
390+
template = <<~LIQUID.chomp
391+
{% tablerow i in (1..3) cols: 2, %}{{ i }}{% endtablerow %}
392+
LIQUID
393+
394+
expected = <<~OUTPUT
395+
<tr class="row1">
396+
<td class="col1">1</td><td class="col2">2</td></tr>
397+
<tr class="row2"><td class="col1">3</td></tr>
398+
OUTPUT
399+
400+
result = Template.parse(template, environment: rigid_environment).render
401+
assert_equal(expected, result)
402+
end
403+
404+
def test_tablerow_with_invalid_attribute_name_in_rigid_mode
405+
template = '{% tablerow i in (1..10) invalid_attr: 5 %}{{ i }}{% endtablerow %}'
406+
error = assert_raises(SyntaxError) do
407+
Template.parse(template, environment: rigid_environment)
408+
end
409+
assert_equal("Liquid syntax error: Invalid attribute 'invalid_attr' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid_attr: 5\"", error.message)
410+
end
411+
412+
def test_tablerow_with_invalid_expression_in_limit_in_rigid_mode
413+
template = '{% tablerow i in (1..10) limit: foo=>bar %}{{ i }}{% endtablerow %}'
414+
error = assert_raises(SyntaxError) do
415+
Template.parse(template, environment: rigid_environment)
416+
end
417+
assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) limit: foo=>bar\"", error.message)
418+
end
419+
420+
def test_tablerow_with_invalid_expression_in_offset_in_rigid_mode
421+
template = '{% tablerow i in (1..10) offset: foo=>bar %}{{ i }}{% endtablerow %}'
422+
error = assert_raises(SyntaxError) do
423+
Template.parse(template, environment: rigid_environment)
424+
end
425+
assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) offset: foo=>bar\"", error.message)
426+
end
427+
428+
def test_tablerow_with_invalid_expression_in_cols_in_rigid_mode
429+
template = '{% tablerow i in (1..10) cols: foo=>bar %}{{ i }}{% endtablerow %}'
430+
error = assert_raises(SyntaxError) do
431+
Template.parse(template, environment: rigid_environment)
432+
end
433+
assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) cols: foo=>bar\"", error.message)
434+
end
435+
436+
def test_tablerow_with_invalid_expression_in_range_in_rigid_mode
437+
template = '{% tablerow i in (1..10) range: foo=>bar %}{{ i }}{% endtablerow %}'
438+
error = assert_raises(SyntaxError) do
439+
Template.parse(template, environment: rigid_environment)
440+
end
441+
assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) range: foo=>bar\"", error.message)
442+
end
443+
444+
def test_tablerow_without_in_keyword_in_rigid_mode
445+
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
446+
error = assert_raises(SyntaxError) do
447+
Template.parse(template, environment: rigid_environment)
448+
end
449+
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
450+
end
451+
452+
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_rigid_mode
453+
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
454+
error = assert_raises(SyntaxError) do
455+
Template.parse(template, environment: rigid_environment)
456+
end
457+
assert_equal("Liquid syntax error: Invalid attribute 'invalid1' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid1: 5, invalid2: 10\"", error.message)
458+
end
459+
460+
def test_tablerow_with_empty_collection_in_rigid_mode
461+
template = <<~LIQUID.chomp
462+
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
463+
LIQUID
464+
465+
expected = <<~OUTPUT
466+
<tr class="row1">
467+
</tr>
468+
OUTPUT
469+
470+
result = Template.parse(template, environment: rigid_environment).render('empty_array' => [])
471+
assert_equal(expected, result)
472+
end
473+
474+
def test_tablerow_lax_mode_still_accepts_invalid_attributes
475+
template = <<~LIQUID.chomp
476+
{% tablerow i in (1..3) invalid_attr: 5 %}{{ i }}{% endtablerow %}
477+
LIQUID
478+
479+
expected = <<~OUTPUT
480+
<tr class="row1">
481+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
482+
OUTPUT
483+
484+
result = Template.parse(template, environment: lax_environment).render
485+
assert_equal(expected, result)
486+
end
487+
488+
def test_tablerow_strict_mode_still_accepts_invalid_attributes
489+
template = <<~LIQUID.chomp
490+
{% tablerow i in (1..3) invalid_attr: 5 %}{{ i }}{% endtablerow %}
491+
LIQUID
492+
493+
expected = <<~OUTPUT
494+
<tr class="row1">
495+
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
496+
OUTPUT
497+
498+
result = Template.parse(template, environment: strict_environment).render
499+
assert_equal(expected, result)
500+
end
501+
502+
private
503+
504+
def rigid_environment
505+
Environment.build(error_mode: :rigid)
506+
end
507+
508+
def strict_environment
509+
Environment.build(error_mode: :strict)
510+
end
511+
512+
def lax_environment
513+
Environment.build(error_mode: :lax)
514+
end
258515
end

test/unit/rigid_mode_unit_test.rb

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ class RigidModeUnitTest < Minitest::Test
66
include Liquid
77

88
def test_tablerow_limit_with_invalid_expression
9-
skip
10-
119
template = <<~LIQUID
12-
{% tablerow i in (1..10) limit: foo=>bar %}{{ i }}{% endtablerow %}
10+
{% tablerow i in (1..10) limit: foo=>bar %}
11+
{{ i }}
12+
{% endtablerow %}
1313
LIQUID
1414

1515
refute_nil(lax_parse(template))
@@ -22,10 +22,10 @@ def test_tablerow_limit_with_invalid_expression
2222
end
2323

2424
def test_tablerow_offset_with_invalid_expression
25-
skip
26-
2725
template = <<~LIQUID
28-
{% tablerow i in (1..10) offset: foo=>bar %}{{ i }}{% endtablerow %}
26+
{% tablerow i in (1..10) offset: foo=>bar %}
27+
{{ i }}
28+
{% endtablerow %}
2929
LIQUID
3030

3131
refute_nil(lax_parse(template))
@@ -37,6 +37,24 @@ def test_tablerow_offset_with_invalid_expression
3737
assert_match(/Unexpected character =/, error.message)
3838
end
3939

40+
def test_tablerow_with_invalid_attribute
41+
template = <<~LIQUID
42+
{% tablerow i in (1..10) invalid_attr: 5 %}
43+
{{ i }}
44+
{% endtablerow %}
45+
LIQUID
46+
47+
refute_nil(lax_parse(template))
48+
refute_nil(strict_parse(template))
49+
50+
error = assert_raises(SyntaxError) do
51+
rigid_parse(template)
52+
end
53+
54+
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
55+
assert_match(/Valid attributes are cols, limit, offset, and range/, error.message)
56+
end
57+
4058
def test_cycle_name_with_invalid_expression
4159
template = <<~LIQUID
4260
{% for i in (1..3) %}

0 commit comments

Comments
 (0)