diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml
index cd1607b3..b99d490c 100644
--- a/lib/liquid/locales/en.yml
+++ b/lib/liquid/locales/en.yml
@@ -20,6 +20,7 @@
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
+ table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
diff --git a/lib/liquid/tags/table_row.rb b/lib/liquid/tags/table_row.rb
index 6767e4fb..11aa4ef5 100644
--- a/lib/liquid/tags/table_row.rb
+++ b/lib/liquid/tags/table_row.rb
@@ -25,11 +25,49 @@ module Liquid
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
+ ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
+ parse_with_selected_parser(markup)
+ end
+
+ def rigid_parse(markup)
+ p = @parse_context.new_parser(markup)
+
+ @variable_name = p.consume(:id)
+
+ unless p.id?("in")
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
+ end
+
+ @collection_name = safe_parse_expression(p)
+
+ # optional comma
+ p.consume?(:comma)
+
+ @attributes = {}
+ while p.look(:id)
+ key = p.consume
+ unless ALLOWED_ATTRIBUTES.include?(key)
+ raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
+ end
+
+ p.consume(:colon)
+ @attributes[key] = safe_parse_expression(p)
+ p.consume?(:comma) # optional comma
+ end
+
+ p.consume(:end_of_string)
+ end
+
+ def strict_parse(markup)
+ lax_parse(markup)
+ end
+
+ def lax_parse(markup)
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2))
diff --git a/test/integration/tags/table_row_test.rb b/test/integration/tags/table_row_test.rb
index ddc0877d..ad12d76f 100644
--- a/test/integration/tags/table_row_test.rb
+++ b/test/integration/tags/table_row_test.rb
@@ -255,4 +255,261 @@ class TableRowTest < Minitest::Test
template,
)
end
+
+ def test_tablerow_with_cols_attribute_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ | 4 | 5 | 6 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_limit_attribute_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_offset_attribute_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 3 | 4 | 5 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_range_attribute_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_multiple_attributes_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 2 | 3 |
+ | 4 | 5 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_variable_collection_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 |
+ | 3 | 4 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render('numbers' => [1, 2, 3, 4])
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_dotted_access_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 |
+ | 3 | 4 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render('obj' => { 'numbers' => [1, 2, 3, 4] })
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_bracketed_access_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 10 | 20 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render('obj' => { 'numbers' => [10, 20] })
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_without_attributes_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_trailing_comma_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..3) cols: 2, %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 |
+ | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_with_invalid_attribute_name_in_rigid_mode
+ template = '{% tablerow i in (1..10) invalid_attr: 5 %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ 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)
+ end
+
+ def test_tablerow_with_invalid_expression_in_limit_in_rigid_mode
+ template = '{% tablerow i in (1..10) limit: foo=>bar %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) limit: foo=>bar\"", error.message)
+ end
+
+ def test_tablerow_with_invalid_expression_in_offset_in_rigid_mode
+ template = '{% tablerow i in (1..10) offset: foo=>bar %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) offset: foo=>bar\"", error.message)
+ end
+
+ def test_tablerow_with_invalid_expression_in_cols_in_rigid_mode
+ template = '{% tablerow i in (1..10) cols: foo=>bar %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) cols: foo=>bar\"", error.message)
+ end
+
+ def test_tablerow_with_invalid_expression_in_range_in_rigid_mode
+ template = '{% tablerow i in (1..10) range: foo=>bar %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ assert_equal("Liquid syntax error: Unexpected character = in \"i in (1..10) range: foo=>bar\"", error.message)
+ end
+
+ def test_tablerow_without_in_keyword_in_rigid_mode
+ template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
+ end
+
+ def test_tablerow_with_multiple_invalid_attributes_reports_first_in_rigid_mode
+ template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
+ error = assert_raises(SyntaxError) do
+ Template.parse(template, environment: rigid_environment)
+ end
+ 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)
+ end
+
+ def test_tablerow_with_empty_collection_in_rigid_mode
+ template = <<~LIQUID.chomp
+ {% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+
+ OUTPUT
+
+ result = Template.parse(template, environment: rigid_environment).render('empty_array' => [])
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_lax_mode_still_accepts_invalid_attributes
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..3) invalid_attr: 5 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: lax_environment).render
+ assert_equal(expected, result)
+ end
+
+ def test_tablerow_strict_mode_still_accepts_invalid_attributes
+ template = <<~LIQUID.chomp
+ {% tablerow i in (1..3) invalid_attr: 5 %}{{ i }}{% endtablerow %}
+ LIQUID
+
+ expected = <<~OUTPUT
+
+ | 1 | 2 | 3 |
+ OUTPUT
+
+ result = Template.parse(template, environment: strict_environment).render
+ assert_equal(expected, result)
+ end
+
+ private
+
+ def rigid_environment
+ Environment.build(error_mode: :rigid)
+ end
+
+ def strict_environment
+ Environment.build(error_mode: :strict)
+ end
+
+ def lax_environment
+ Environment.build(error_mode: :lax)
+ end
end
diff --git a/test/unit/rigid_mode_unit_test.rb b/test/unit/rigid_mode_unit_test.rb
index cb0de7c4..60ac0423 100644
--- a/test/unit/rigid_mode_unit_test.rb
+++ b/test/unit/rigid_mode_unit_test.rb
@@ -6,10 +6,10 @@ class RigidModeUnitTest < Minitest::Test
include Liquid
def test_tablerow_limit_with_invalid_expression
- skip
-
template = <<~LIQUID
- {% tablerow i in (1..10) limit: foo=>bar %}{{ i }}{% endtablerow %}
+ {% tablerow i in (1..10) limit: foo=>bar %}
+ {{ i }}
+ {% endtablerow %}
LIQUID
refute_nil(lax_parse(template))
@@ -22,10 +22,10 @@ class RigidModeUnitTest < Minitest::Test
end
def test_tablerow_offset_with_invalid_expression
- skip
-
template = <<~LIQUID
- {% tablerow i in (1..10) offset: foo=>bar %}{{ i }}{% endtablerow %}
+ {% tablerow i in (1..10) offset: foo=>bar %}
+ {{ i }}
+ {% endtablerow %}
LIQUID
refute_nil(lax_parse(template))
@@ -37,6 +37,24 @@ class RigidModeUnitTest < Minitest::Test
assert_match(/Unexpected character =/, error.message)
end
+ def test_tablerow_with_invalid_attribute
+ template = <<~LIQUID
+ {% tablerow i in (1..10) invalid_attr: 5 %}
+ {{ i }}
+ {% endtablerow %}
+ LIQUID
+
+ refute_nil(lax_parse(template))
+ refute_nil(strict_parse(template))
+
+ error = assert_raises(SyntaxError) do
+ rigid_parse(template)
+ end
+
+ assert_match(/Invalid attribute 'invalid_attr'/, error.message)
+ assert_match(/Valid attributes are cols, limit, offset, and range/, error.message)
+ end
+
def test_cycle_name_with_invalid_expression
template = <<~LIQUID
{% for i in (1..3) %}