From 03a6d4f8b26454f580921756969196ef9fd3f787 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 15 Jan 2025 23:10:51 +0100 Subject: [PATCH 1/5] Add support to LiquidDoc with the new `{% doc %}` tag --- lib/liquid/tags.rb | 2 + lib/liquid/tags/comment.rb | 17 ++- lib/liquid/tags/doc.rb | 30 +++++ test/unit/block_unit_test.rb | 8 +- test/unit/tags/doc_tag_unit_test.rb | 199 ++++++++++++++++++++++++++++ 5 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 lib/liquid/tags/doc.rb create mode 100644 test/unit/tags/doc_tag_unit_test.rb diff --git a/lib/liquid/tags.rb b/lib/liquid/tags.rb index 916a63bd5..dff7553f7 100644 --- a/lib/liquid/tags.rb +++ b/lib/liquid/tags.rb @@ -19,6 +19,7 @@ require_relative "tags/raw" require_relative "tags/render" require_relative "tags/cycle" +require_relative "tags/doc" module Liquid module Tags @@ -42,6 +43,7 @@ module Tags 'if' => If, 'echo' => Echo, 'tablerow' => TableRow, + 'doc' => Doc, }.freeze end end diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 659108e37..0a6c02c0c 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -15,6 +15,8 @@ module Liquid # {% endcomment %} # @liquid_syntax_keyword content The content of the comment. class Comment < Block + TAG_NAME = "comment" + def render_to_output_buffer(_context, output) output end @@ -34,7 +36,10 @@ def parse_body(body, tokenizer) end parse_context.depth += 1 - comment_tag_depth = 1 + tag_depth = 1 + + begin_tag = self.class::TAG_NAME + end_tag = "end#{self.class::TAG_NAME}" begin # Consume tokens without creating child nodes. @@ -57,13 +62,13 @@ def parse_body(body, tokenizer) case tag_name when "raw" parse_raw_tag_body(tokenizer) - when "comment" - comment_tag_depth += 1 - when "endcomment" - comment_tag_depth -= 1 + when begin_tag + tag_depth += 1 + when end_tag + tag_depth -= 1 end - if comment_tag_depth.zero? + if tag_depth.zero? parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag return false end diff --git a/lib/liquid/tags/doc.rb b/lib/liquid/tags/doc.rb new file mode 100644 index 000000000..ae16b4aab --- /dev/null +++ b/lib/liquid/tags/doc.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Liquid + # @liquid_public_docs + # @liquid_type tag + # @liquid_category syntax + # @liquid_name doc + # @liquid_summary + # Documents template elements with annotations. + # @liquid_description + # The `doc` tag allows developers to include documentation within Liquid + # templates. Any content inside `doc` tags is not rendered or outputted. + # Liquid code inside will be parsed but not executed. This facilitates + # tooling support for features like code completion, linting, and inline + # documentation. + # @liquid_syntax + # {% doc %} + # Renders a message. + # + # @param {string} foo - A foo value. + # @param {string} [bar] - An optional bar value. + # + # @example + # {% render 'message', foo: 'Hello', bar: 'World' %} + # {% enddoc %} + # {{ foo }}, {{ bar }}! + class Doc < Comment + TAG_NAME = "doc" + end +end diff --git a/test/unit/block_unit_test.rb b/test/unit/block_unit_test.rb index f28c30e16..cfaf3f663 100644 --- a/test/unit/block_unit_test.rb +++ b/test/unit/block_unit_test.rb @@ -47,12 +47,18 @@ def test_variable_many_embedded_fragments ) end - def test_with_block + def test_comment_tag_with_block template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") assert_equal([String, Comment, String], block_types(template.root.nodelist)) assert_equal(3, template.root.nodelist.size) end + def test_doc_tag_with_block + template = Liquid::Template.parse(" {% doc %} {% enddoc %} ") + assert_equal([String, Doc, String], block_types(template.root.nodelist)) + assert_equal(3, template.root.nodelist.size) + end + private def block_types(nodelist) diff --git a/test/unit/tags/doc_tag_unit_test.rb b/test/unit/tags/doc_tag_unit_test.rb new file mode 100644 index 000000000..50e92a4c1 --- /dev/null +++ b/test/unit/tags/doc_tag_unit_test.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DocTagUnitTest < Minitest::Test + def test_doc_inside_liquid_tag + assert_template_result('', <<~LIQUID.chomp) + {% liquid + if 1 != 1 + doc + else + echo 123 + enddoc + endif + %} + LIQUID + end + + def test_does_not_parse_nodes_inside_a_doc + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% if true %} + {% if ... %} + {%- for ? -%} + {% while true %} + {% + unless if + %} + {% endcase %} + {% enddoc %} + LIQUID + end + + def test_allows_unclosed_tags + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% if true %} + {% enddoc %} + LIQUID + end + + def test_open_tags_in_doc + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% assign a = 123 {% doc %} + {% enddoc %} + LIQUID + + assert_raises(Liquid::SyntaxError) do + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% assign foo = "1" + {% enddoc %} + LIQUID + end + + assert_raises(Liquid::SyntaxError) do + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% doc %} + {% invalid + {% enddoc %} + {% enddoc %} + LIQUID + end + + assert_raises(Liquid::SyntaxError) do + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% {{ {%- enddoc %} + LIQUID + end + end + + def test_child_doc_tags_need_to_be_closed + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% doc %} + {% doc %}{% enddoc %} + {% enddoc %} + {% enddoc %} + LIQUID + + assert_raises(Liquid::SyntaxError) do + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% doc %} + {% doc %} + {% enddoc %} + {% enddoc %} + LIQUID + end + end + + def test_child_raw_tags_need_to_be_closed + assert_template_result('', <<~LIQUID.chomp) + {% doc %} + {% raw %} + {% enddoc %} + {% endraw %} + {% enddoc %} + LIQUID + + assert_raises(Liquid::SyntaxError) do + Liquid::Template.parse(<<~LIQUID.chomp) + {% doc %} + {% raw %} + {% enddoc %} + {% enddoc %} + LIQUID + end + end + + def test_error_line_number_is_correct + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true) + {% doc %} + {% if true %} + {% enddoc %} + {{ errors.standard_error }} + LIQUID + + output = template.render('errors' => ErrorDrop.new) + expected = <<~TEXT.chomp + + Liquid error (line 4): standard error + TEXT + + assert_equal(expected, output) + end + + def test_doc_tag_delimiter_with_extra_strings + assert_template_result( + '', + <<~LIQUID.chomp, + {% doc %} + {% doc %} + {% enddoc + {% if true %} + {% endif %} + {% enddoc %} + LIQUID + ) + end + + def test_nested_doc_tag_with_extra_strings + assert_template_result( + '', + <<~LIQUID.chomp, + {% doc %} + {% doc + {% assign foo = 1 %} + {% enddoc + {% assign foo = 1 %} + {% enddoc %} + LIQUID + ) + end + + def test_ignores_delimiter_with_extra_strings + assert_template_result('', <<~LIQUID.chomp) + {% if true %} + {% doc %} + {% docEXTRA %}wut{% enddocEXTRA %}xyz + {% enddoc %} + {% endif %} + LIQUID + end + + def test_delimiter_can_have_extra_strings + assert_template_result('', "{% doc %}123{% enddoc xyz %}") + assert_template_result('', "{% doc %}123{% enddoc\txyz %}") + assert_template_result('', "{% doc %}123{% enddoc\nxyz %}") + assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}") + assert_template_result('', "{%doc}{% assign a = 1 %}{%enddoc}{% endif %}") + end + + def test_with_whitespace_control + assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!") + assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!") + assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!") + + assert_template_result("Hello!", <<~LIQUID.chomp) + {%- doc %}Whitespace control!{% enddoc -%} + Hello! + LIQUID + end + + def test_dont_override_liquid_tag_whitespace_control + assert_template_result("Hello!World!", <<~LIQUID.chomp) + Hello! + {%- liquid + doc + this is inside a liquid tag + enddoc + -%} + World! + LIQUID + end +end From 09a51bd6e8bddd8a4694892add6d8da7d1045485 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Tue, 21 Jan 2025 12:18:55 +0100 Subject: [PATCH 2/5] Update `{% doc %}` to no longer support nested tags (as {`% comment %}` does) --- lib/liquid/locales/en.yml | 1 + lib/liquid/tags/comment.rb | 17 +-- lib/liquid/tags/doc.rb | 45 +++++- test/unit/tags/doc_tag_unit_test.rb | 211 ++++++++++++++-------------- 4 files changed, 158 insertions(+), 116 deletions(-) diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml index f33c61cec..60307ac79 100644 --- a/lib/liquid/locales/en.yml +++ b/lib/liquid/locales/en.yml @@ -8,6 +8,7 @@ case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" + doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed" for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]" for_invalid_in: "For loops require an 'in' clause" for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 0a6c02c0c..659108e37 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -15,8 +15,6 @@ module Liquid # {% endcomment %} # @liquid_syntax_keyword content The content of the comment. class Comment < Block - TAG_NAME = "comment" - def render_to_output_buffer(_context, output) output end @@ -36,10 +34,7 @@ def parse_body(body, tokenizer) end parse_context.depth += 1 - tag_depth = 1 - - begin_tag = self.class::TAG_NAME - end_tag = "end#{self.class::TAG_NAME}" + comment_tag_depth = 1 begin # Consume tokens without creating child nodes. @@ -62,13 +57,13 @@ def parse_body(body, tokenizer) case tag_name when "raw" parse_raw_tag_body(tokenizer) - when begin_tag - tag_depth += 1 - when end_tag - tag_depth -= 1 + when "comment" + comment_tag_depth += 1 + when "endcomment" + comment_tag_depth -= 1 end - if tag_depth.zero? + if comment_tag_depth.zero? parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag return false end diff --git a/lib/liquid/tags/doc.rb b/lib/liquid/tags/doc.rb index ae16b4aab..e798ec653 100644 --- a/lib/liquid/tags/doc.rb +++ b/lib/liquid/tags/doc.rb @@ -24,7 +24,48 @@ module Liquid # {% render 'message', foo: 'Hello', bar: 'World' %} # {% enddoc %} # {{ foo }}, {{ bar }}! - class Doc < Comment - TAG_NAME = "doc" + class Doc < Block + def render_to_output_buffer(_context, output) + output + end + + def unknown_tag(_tag, _markup, _tokens) + end + + def blank? + true + end + + def parse_body(body, tokenizer) + while (token = tokenizer.send(:shift)) + tag_name = if tokenizer.for_liquid_tag + next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing) + + tag_name_match = BlockBody::LiquidTagToken.match(token) + + next if tag_name_match.nil? + + tag_name_match[1] + else + token =~ BlockBody::FullToken + Regexp.last_match(2) + end + + raise_nested_doc_error if tag_name == "doc" + + if tag_name == "enddoc" + parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag + return false + end + end + + raise_tag_never_closed(block_name) + end + + private + + def raise_nested_doc_error + raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested") + end end end diff --git a/test/unit/tags/doc_tag_unit_test.rb b/test/unit/tags/doc_tag_unit_test.rb index 50e92a4c1..5ad817443 100644 --- a/test/unit/tags/doc_tag_unit_test.rb +++ b/test/unit/tags/doc_tag_unit_test.rb @@ -3,8 +3,38 @@ require 'test_helper' class DocTagUnitTest < Minitest::Test - def test_doc_inside_liquid_tag - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag + template = <<~LIQUID.chomp + {% doc %} + Renders loading-spinner. + + @param {string} foo - some foo + @param {string} [bar] - optional bar + + @example + {% render 'loading-spinner', foo: 'foo' %} + {% render 'loading-spinner', foo: 'foo', bar: 'bar' %} + {% enddoc %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_inside_liquid_tag + template = <<~LIQUID.chomp + {% liquid + doc + Assigns foo to 1. + enddoc + assign foo = 1 + %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_inside_liquid_tag_with_control_flow_nodes + template = <<~LIQUID.chomp {% liquid if 1 != 1 doc @@ -14,10 +44,12 @@ def test_doc_inside_liquid_tag endif %} LIQUID + + assert_template_result('', template) end - def test_does_not_parse_nodes_inside_a_doc - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag_ignores_liquid_nodes + template = <<~LIQUID.chomp {% doc %} {% if true %} {% if ... %} @@ -29,89 +61,92 @@ def test_does_not_parse_nodes_inside_a_doc {% endcase %} {% enddoc %} LIQUID + + assert_template_result('', template) end - def test_allows_unclosed_tags - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag_ignores_unclosed_liquid_tags + template = <<~LIQUID.chomp {% doc %} {% if true %} {% enddoc %} LIQUID + + assert_template_result('', template) end - def test_open_tags_in_doc - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag_ignores_nested_doc_tags + template = <<~LIQUID.chomp {% doc %} {% assign a = 123 {% doc %} {% enddoc %} LIQUID - assert_raises(Liquid::SyntaxError) do - assert_template_result('', <<~LIQUID.chomp) - {% doc %} - {% assign foo = "1" - {% enddoc %} - LIQUID - end + assert_template_result('', template) + end - assert_raises(Liquid::SyntaxError) do - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag_does_not_allow_nested_docs + error = assert_raises(Liquid::SyntaxError) do + template = <<~LIQUID.chomp {% doc %} {% doc %} - {% invalid - {% enddoc %} + {% doc %} {% enddoc %} LIQUID - end - assert_raises(Liquid::SyntaxError) do - assert_template_result('', <<~LIQUID.chomp) - {% doc %} - {% {{ {%- enddoc %} - LIQUID + Liquid::Template.parse(template) end + + exp_error = "Liquid syntax error: Syntax Error in 'doc' - Nested doc tags are not allowed" + act_error = error.message + + assert_equal(exp_error, act_error) end - def test_child_doc_tags_need_to_be_closed - assert_template_result('', <<~LIQUID.chomp) + def test_doc_tag_ignores_nested_raw_tags + template = <<~LIQUID.chomp {% doc %} - {% doc %} - {% doc %}{% enddoc %} - {% enddoc %} + {% raw %} {% enddoc %} LIQUID - assert_raises(Liquid::SyntaxError) do - assert_template_result('', <<~LIQUID.chomp) + assert_template_result('', template) + end + + def test_doc_tag_raises_an_error_for_unclosed_assign + error = assert_raises(Liquid::SyntaxError) do + template = <<~LIQUID.chomp {% doc %} - {% doc %} - {% doc %} - {% enddoc %} + {% assign foo = "1" {% enddoc %} LIQUID + + Liquid::Template.parse(template) end - end - def test_child_raw_tags_need_to_be_closed - assert_template_result('', <<~LIQUID.chomp) - {% doc %} - {% raw %} - {% enddoc %} - {% endraw %} - {% enddoc %} - LIQUID + exp_error = "Liquid syntax error: 'doc' tag was never closed" + act_error = error.message + + assert_equal(exp_error, act_error) + end - assert_raises(Liquid::SyntaxError) do - Liquid::Template.parse(<<~LIQUID.chomp) + def test_doc_tag_raises_an_error_for_malformed_syntax + error = assert_raises(Liquid::SyntaxError) do + template = <<~LIQUID.chomp {% doc %} - {% raw %} - {% enddoc %} - {% enddoc %} + {% {{ {%- enddoc %} LIQUID + + Liquid::Template.parse(template) end + + exp_error = "Liquid syntax error: 'doc' tag was never closed" + act_error = error.message + + assert_equal(exp_error, act_error) end - def test_error_line_number_is_correct + def test_doc_tag_preserves_error_line_numbers template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true) {% doc %} {% if true %} @@ -119,44 +154,39 @@ def test_error_line_number_is_correct {{ errors.standard_error }} LIQUID - output = template.render('errors' => ErrorDrop.new) expected = <<~TEXT.chomp Liquid error (line 4): standard error TEXT - assert_equal(expected, output) + assert_equal(expected, template.render('errors' => ErrorDrop.new)) end - def test_doc_tag_delimiter_with_extra_strings - assert_template_result( - '', - <<~LIQUID.chomp, - {% doc %} - {% doc %} - {% enddoc - {% if true %} - {% endif %} - {% enddoc %} - LIQUID - ) - end + def test_doc_tag_whitespace_control + # Basic whitespace control + assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!") + assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!") + assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!") - def test_nested_doc_tag_with_extra_strings - assert_template_result( - '', - <<~LIQUID.chomp, - {% doc %} - {% doc - {% assign foo = 1 %} - {% enddoc - {% assign foo = 1 %} - {% enddoc %} - LIQUID - ) + # Whitespace control within liquid tags + assert_template_result("Hello!World!", <<~LIQUID.chomp) + Hello! + {%- liquid + doc + this is inside a liquid tag + enddoc + -%} + World! + LIQUID + + # Multiline whitespace control + assert_template_result("Hello!", <<~LIQUID.chomp) + {%- doc %}Whitespace control!{% enddoc -%} + Hello! + LIQUID end - def test_ignores_delimiter_with_extra_strings + def test_doc_tag_delimiter_handling assert_template_result('', <<~LIQUID.chomp) {% if true %} {% doc %} @@ -164,36 +194,11 @@ def test_ignores_delimiter_with_extra_strings {% enddoc %} {% endif %} LIQUID - end - def test_delimiter_can_have_extra_strings assert_template_result('', "{% doc %}123{% enddoc xyz %}") assert_template_result('', "{% doc %}123{% enddoc\txyz %}") assert_template_result('', "{% doc %}123{% enddoc\nxyz %}") assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}") assert_template_result('', "{%doc}{% assign a = 1 %}{%enddoc}{% endif %}") end - - def test_with_whitespace_control - assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!") - assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!") - assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!") - - assert_template_result("Hello!", <<~LIQUID.chomp) - {%- doc %}Whitespace control!{% enddoc -%} - Hello! - LIQUID - end - - def test_dont_override_liquid_tag_whitespace_control - assert_template_result("Hello!World!", <<~LIQUID.chomp) - Hello! - {%- liquid - doc - this is inside a liquid tag - enddoc - -%} - World! - LIQUID - end end From 677d289e46d40612c359387dce22a8446c0f6e79 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 22 Jan 2025 16:15:06 +0100 Subject: [PATCH 3/5] Remove misleading unit test (thank you, @EvilGenius13) --- test/unit/tags/doc_tag_unit_test.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/unit/tags/doc_tag_unit_test.rb b/test/unit/tags/doc_tag_unit_test.rb index 5ad817443..d277ef058 100644 --- a/test/unit/tags/doc_tag_unit_test.rb +++ b/test/unit/tags/doc_tag_unit_test.rb @@ -75,16 +75,6 @@ def test_doc_tag_ignores_unclosed_liquid_tags assert_template_result('', template) end - def test_doc_tag_ignores_nested_doc_tags - template = <<~LIQUID.chomp - {% doc %} - {% assign a = 123 {% doc %} - {% enddoc %} - LIQUID - - assert_template_result('', template) - end - def test_doc_tag_does_not_allow_nested_docs error = assert_raises(Liquid::SyntaxError) do template = <<~LIQUID.chomp From f407157cbd5774d90d37d9b7e487ad44965d7f46 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Mon, 17 Feb 2025 14:57:11 +0100 Subject: [PATCH 4/5] Update the implementation to make {% doc %} as strict as {% raw %} --- lib/liquid/locales/en.yml | 1 + lib/liquid/tags/doc.rb | 51 ++++++++-------- test/unit/tags/doc_tag_unit_test.rb | 94 +++++++++-------------------- 3 files changed, 56 insertions(+), 90 deletions(-) diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml index 60307ac79..cd1607b3c 100644 --- a/lib/liquid/locales/en.yml +++ b/lib/liquid/locales/en.yml @@ -2,6 +2,7 @@ errors: syntax: tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}" + block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}" assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" case: "Syntax Error in 'case' - Valid syntax: case [condition]" diff --git a/lib/liquid/tags/doc.rb b/lib/liquid/tags/doc.rb index e798ec653..c345e4c21 100644 --- a/lib/liquid/tags/doc.rb +++ b/lib/liquid/tags/doc.rb @@ -25,45 +25,44 @@ module Liquid # {% enddoc %} # {{ foo }}, {{ bar }}! class Doc < Block - def render_to_output_buffer(_context, output) - output - end + TAG_UNEXPECTED_ARGS = /\A\s*\z/ - def unknown_tag(_tag, _markup, _tokens) + def initialize(tag_name, markup, parse_context) + super + ensure_valid_markup(tag_name, markup, parse_context) end - def blank? - true - end - - def parse_body(body, tokenizer) - while (token = tokenizer.send(:shift)) - tag_name = if tokenizer.for_liquid_tag - next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing) - - tag_name_match = BlockBody::LiquidTagToken.match(token) - - next if tag_name_match.nil? - - tag_name_match[1] - else - token =~ BlockBody::FullToken - Regexp.last_match(2) - end + def parse(tokens) + while (token = tokens.shift) + tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2) - raise_nested_doc_error if tag_name == "doc" + raise_nested_doc_error if tag_name == @tag_name - if tag_name == "enddoc" - parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag - return false + if tag_name == block_delimiter + parse_context.trim_whitespace = (token[-3] == WhitespaceControl) + return end end raise_tag_never_closed(block_name) end + def render_to_output_buffer(_context, output) + output + end + + def blank? + true + end + private + def ensure_valid_markup(tag_name, markup, parse_context) + unless TAG_UNEXPECTED_ARGS.match?(markup) + raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name) + end + end + def raise_nested_doc_error raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested") end diff --git a/test/unit/tags/doc_tag_unit_test.rb b/test/unit/tags/doc_tag_unit_test.rb index d277ef058..861c1b6ca 100644 --- a/test/unit/tags/doc_tag_unit_test.rb +++ b/test/unit/tags/doc_tag_unit_test.rb @@ -20,32 +20,26 @@ def test_doc_tag assert_template_result('', template) end - def test_doc_tag_inside_liquid_tag - template = <<~LIQUID.chomp - {% liquid - doc - Assigns foo to 1. - enddoc - assign foo = 1 - %} - LIQUID + def test_doc_tag_does_not_support_extra_arguments + error = assert_raises(Liquid::SyntaxError) do + template = <<~LIQUID.chomp + {% doc extra %} + {% enddoc %} + LIQUID - assert_template_result('', template) - end + Liquid::Template.parse(template) + end - def test_doc_tag_inside_liquid_tag_with_control_flow_nodes - template = <<~LIQUID.chomp - {% liquid - if 1 != 1 - doc - else - echo 123 - enddoc - endif - %} - LIQUID + exp_error = "Liquid syntax error: Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}" + act_error = error.message - assert_template_result('', template) + assert_equal(exp_error, act_error) + end + + def test_doc_tag_must_support_valid_tags + assert_match_syntax_error("Liquid syntax error (line 1): 'doc' tag was never closed", '{% doc %} foo') + assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo {% enddoc %}') + assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo %}{% enddoc %}') end def test_doc_tag_ignores_liquid_nodes @@ -103,37 +97,23 @@ def test_doc_tag_ignores_nested_raw_tags assert_template_result('', template) end - def test_doc_tag_raises_an_error_for_unclosed_assign - error = assert_raises(Liquid::SyntaxError) do - template = <<~LIQUID.chomp - {% doc %} - {% assign foo = "1" - {% enddoc %} - LIQUID - - Liquid::Template.parse(template) - end - - exp_error = "Liquid syntax error: 'doc' tag was never closed" - act_error = error.message + def test_doc_tag_ignores_unclosed_assign + template = <<~LIQUID.chomp + {% doc %} + {% assign foo = "1" + {% enddoc %} + LIQUID - assert_equal(exp_error, act_error) + assert_template_result('', template) end - def test_doc_tag_raises_an_error_for_malformed_syntax - error = assert_raises(Liquid::SyntaxError) do - template = <<~LIQUID.chomp - {% doc %} - {% {{ {%- enddoc %} - LIQUID - - Liquid::Template.parse(template) - end - - exp_error = "Liquid syntax error: 'doc' tag was never closed" - act_error = error.message + def test_doc_tag_ignores_malformed_syntax + template = <<~LIQUID.chomp + {% doc %} + {% {{ {%- enddoc %} + LIQUID - assert_equal(exp_error, act_error) + assert_template_result('', template) end def test_doc_tag_preserves_error_line_numbers @@ -157,19 +137,6 @@ def test_doc_tag_whitespace_control assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!") assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!") assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!") - - # Whitespace control within liquid tags - assert_template_result("Hello!World!", <<~LIQUID.chomp) - Hello! - {%- liquid - doc - this is inside a liquid tag - enddoc - -%} - World! - LIQUID - - # Multiline whitespace control assert_template_result("Hello!", <<~LIQUID.chomp) {%- doc %}Whitespace control!{% enddoc -%} Hello! @@ -189,6 +156,5 @@ def test_doc_tag_delimiter_handling assert_template_result('', "{% doc %}123{% enddoc\txyz %}") assert_template_result('', "{% doc %}123{% enddoc\nxyz %}") assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}") - assert_template_result('', "{%doc}{% assign a = 1 %}{%enddoc}{% endif %}") end end From cae1041ee26418f0b8d62d8fd8a16cf989bef6e8 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Mon, 17 Feb 2025 14:59:49 +0100 Subject: [PATCH 5/5] Rename {% doc %} constant strictly validates the abscense of args --- lib/liquid/tags/doc.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/liquid/tags/doc.rb b/lib/liquid/tags/doc.rb index c345e4c21..f2c299dd7 100644 --- a/lib/liquid/tags/doc.rb +++ b/lib/liquid/tags/doc.rb @@ -25,7 +25,7 @@ module Liquid # {% enddoc %} # {{ foo }}, {{ bar }}! class Doc < Block - TAG_UNEXPECTED_ARGS = /\A\s*\z/ + NO_UNEXPECTED_ARGS = /\A\s*\z/ def initialize(tag_name, markup, parse_context) super @@ -58,7 +58,7 @@ def blank? private def ensure_valid_markup(tag_name, markup, parse_context) - unless TAG_UNEXPECTED_ARGS.match?(markup) + unless NO_UNEXPECTED_ARGS.match?(markup) raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name) end end