diff --git a/lib/liquid/tags.rb b/lib/liquid/tags.rb index 916a63bd..dff7553f 100644 --- a/lib/liquid/tags.rb +++ b/lib/liquid/tags.rb @@ -19,6 +19,7 @@ require_relative "tags/comment" 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 Liquid '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 659108e3..0a6c02c0 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 @@ module Liquid 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 @@ module Liquid 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 00000000..ae16b4aa --- /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 f28c30e1..cfaf3f66 100644 --- a/test/unit/block_unit_test.rb +++ b/test/unit/block_unit_test.rb @@ -47,12 +47,18 @@ class BlockUnitTest < Minitest::Test ) 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 00000000..50e92a4c --- /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