diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml index f33c61cec..cd1607b3c 100644 --- a/lib/liquid/locales/en.yml +++ b/lib/liquid/locales/en.yml @@ -2,12 +2,14 @@ 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]" 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.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/doc.rb b/lib/liquid/tags/doc.rb new file mode 100644 index 000000000..f2c299dd7 --- /dev/null +++ b/lib/liquid/tags/doc.rb @@ -0,0 +1,70 @@ +# 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 < Block + NO_UNEXPECTED_ARGS = /\A\s*\z/ + + def initialize(tag_name, markup, parse_context) + super + ensure_valid_markup(tag_name, markup, parse_context) + end + + def parse(tokens) + while (token = tokens.shift) + tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2) + + raise_nested_doc_error if tag_name == @tag_name + + 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 NO_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 + 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..861c1b6ca --- /dev/null +++ b/test/unit/tags/doc_tag_unit_test.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DocTagUnitTest < Minitest::Test + 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_does_not_support_extra_arguments + error = assert_raises(Liquid::SyntaxError) do + template = <<~LIQUID.chomp + {% doc extra %} + {% enddoc %} + LIQUID + + Liquid::Template.parse(template) + end + + exp_error = "Liquid syntax error: Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}" + act_error = error.message + + 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 + template = <<~LIQUID.chomp + {% doc %} + {% if true %} + {% if ... %} + {%- for ? -%} + {% while true %} + {% + unless if + %} + {% endcase %} + {% enddoc %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_ignores_unclosed_liquid_tags + template = <<~LIQUID.chomp + {% doc %} + {% if true %} + {% 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 + {% doc %} + {% doc %} + {% 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_doc_tag_ignores_nested_raw_tags + template = <<~LIQUID.chomp + {% doc %} + {% raw %} + {% enddoc %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_ignores_unclosed_assign + template = <<~LIQUID.chomp + {% doc %} + {% assign foo = "1" + {% enddoc %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_ignores_malformed_syntax + template = <<~LIQUID.chomp + {% doc %} + {% {{ {%- enddoc %} + LIQUID + + assert_template_result('', template) + end + + def test_doc_tag_preserves_error_line_numbers + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true) + {% doc %} + {% if true %} + {% enddoc %} + {{ errors.standard_error }} + LIQUID + + expected = <<~TEXT.chomp + + Liquid error (line 4): standard error + TEXT + + assert_equal(expected, template.render('errors' => ErrorDrop.new)) + 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!") + assert_template_result("Hello!", <<~LIQUID.chomp) + {%- doc %}Whitespace control!{% enddoc -%} + Hello! + LIQUID + end + + def test_doc_tag_delimiter_handling + assert_template_result('', <<~LIQUID.chomp) + {% if true %} + {% doc %} + {% docEXTRA %}wut{% enddocEXTRA %}xyz + {% enddoc %} + {% endif %} + LIQUID + + 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 %}") + end +end