diff --git a/lib/liquid.rb b/lib/liquid.rb index 4d0a71a64..09cafe947 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -67,6 +67,7 @@ module Liquid require 'liquid/drop' require 'liquid/tablerowloop_drop' require 'liquid/forloop_drop' +require 'liquid/snippet_drop' require 'liquid/extensions' require 'liquid/errors' require 'liquid/interrupts' diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml index b99d490c8..7a9fb71e4 100644 --- a/lib/liquid/locales/en.yml +++ b/lib/liquid/locales/en.yml @@ -5,6 +5,7 @@ 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]" + snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [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) " @@ -19,6 +20,7 @@ invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_template_encoding: "Invalid template encoding" render: "Syntax error in tag 'render' - Template name must be a quoted string" + render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}" 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" @@ -29,5 +31,6 @@ variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" argument: include: "Argument error in tag 'include' - Illegal template name" + render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed" disabled: tag: "usage is not allowed in this context" diff --git a/lib/liquid/snippet_drop.rb b/lib/liquid/snippet_drop.rb new file mode 100644 index 000000000..98686999c --- /dev/null +++ b/lib/liquid/snippet_drop.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Liquid + class SnippetDrop < Drop + attr_reader :body, :name, :filename + + def initialize(body, name, filename) + super() + @body = body + @name = name + @filename = filename + end + + def to_partial + @body + end + + def to_s + 'SnippetDrop' + end + end +end diff --git a/lib/liquid/tags.rb b/lib/liquid/tags.rb index dff7553f7..3ec5e7cfb 100644 --- a/lib/liquid/tags.rb +++ b/lib/liquid/tags.rb @@ -20,6 +20,7 @@ require_relative "tags/render" require_relative "tags/cycle" require_relative "tags/doc" +require_relative "tags/snippet" module Liquid module Tags @@ -44,6 +45,7 @@ module Tags 'echo' => Echo, 'tablerow' => TableRow, 'doc' => Doc, + 'snippet' => Snippet, }.freeze end end diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb index 4d29e420e..dec725ed4 100644 --- a/lib/liquid/tags/render.rb +++ b/lib/liquid/tags/render.rb @@ -27,7 +27,7 @@ module Liquid # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension. class Render < Tag FOR = 'for' - SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o + SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o disable_tags "include" @@ -47,21 +47,23 @@ def render_to_output_buffer(context, output) end def render_tag(context, output) - # The expression should be a String literal, which parses to a String object - template_name = @template_name_expr - raise ::ArgumentError unless template_name.is_a?(String) - - partial = PartialCache.load( - template_name, - context: context, - parse_context: parse_context, - ) - - context_variable_name = @alias_name || template_name.split('/').last + template = context.evaluate(@template_name_expr) + + if template.respond_to?(:to_partial) + partial = template.to_partial + template_name = template.filename + context_variable_name = @alias_name || template.name + elsif @template_name_expr.is_a?(String) + partial = PartialCache.load(template, context: context, parse_context: parse_context) + template_name = partial.name + context_variable_name = @alias_name || template_name.split('/').last + else + raise ::ArgumentError + end render_partial_func = ->(var, forloop) { inner_context = context.new_isolated_subcontext - inner_context.template_name = partial.name + inner_context.template_name = template_name inner_context.partial = true inner_context['forloop'] = forloop if forloop @@ -101,14 +103,18 @@ def rigid_parse(markup) key = p.consume p.consume(:colon) @attributes[key] = safe_parse_expression(p) - p.consume?(:comma) + p.consume?(:comma) # optional comma end p.consume(:end_of_string) end def rigid_template_name(p) - p.consume(:string) + return p.consume(:string) if p.look(:string) + return p.consume(:id) if p.look(:id) + + found = p.consume || "nothing" + raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found) end def strict_parse(markup) diff --git a/lib/liquid/tags/snippet.rb b/lib/liquid/tags/snippet.rb new file mode 100644 index 000000000..87e2a0ded --- /dev/null +++ b/lib/liquid/tags/snippet.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Liquid + # @liquid_public_docs + # @liquid_type tag + # @liquid_category variable + # @liquid_name snippet + # @liquid_summary + # Creates a new inline snippet. + # @liquid_description + # You can create inline snippets to make your Liquid code more modular. + # @liquid_syntax + # {% snippet snippet_name %} + # value + # {% endsnippet %} + class Snippet < Block + def initialize(tag_name, markup, options) + super + p = @parse_context.new_parser(markup) + if p.look(:id) + @to = p.consume(:id) + p.consume(:end_of_string) + else + raise SyntaxError, options[:locale].t("errors.syntax.snippet") + end + end + + def render_to_output_buffer(context, output) + snippet_drop = SnippetDrop.new(@body, @to, context.template_name) + context.scopes.last[@to] = snippet_drop + context.resource_limits.increment_assign_score(assign_score_of(snippet_drop)) + output + end + + def blank? + true + end + + private + + def assign_score_of(snippet_drop) + snippet_drop.body.nodelist.sum { |node| node.to_s.bytesize } + end + end +end diff --git a/test/integration/tags/render_tag_test.rb b/test/integration/tags/render_tag_test.rb index d6453fb51..6fe19abb4 100644 --- a/test/integration/tags/render_tag_test.rb +++ b/test/integration/tags/render_tag_test.rb @@ -101,10 +101,6 @@ def test_sub_contexts_count_towards_the_same_recursion_limit end end - def test_dynamically_choosen_templates_are_not_allowed - assert_syntax_error("{% assign name = 'snippet' %}{% render name %}") - end - def test_rigid_parsing_errors with_error_modes(:lax, :strict) do assert_template_result( @@ -294,6 +290,13 @@ def test_render_tag_with_drop ) end + def test_render_tag_with_snippet_drop + assert_template_result( + "Hello from snippet", + "{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}", + ) + end + def test_render_tag_renders_error_with_template_name assert_template_result( 'Liquid error (foo line 1): standard error', diff --git a/test/integration/tags/snippet_test.rb b/test/integration/tags/snippet_test.rb new file mode 100644 index 000000000..6d1cb35d7 --- /dev/null +++ b/test/integration/tags/snippet_test.rb @@ -0,0 +1,1087 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SnippetTest < Minitest::Test + include Liquid + + class LaxMode < SnippetTest + def test_valid_inline_snippet + template = <<~LIQUID.strip + {% snippet input %} + Hey + {% endsnippet %} + LIQUID + expected = '' + + assert_template_result(expected, template) + end + + def test_render_inline_snippet + template = <<~LIQUID.strip + {% snippet hey %} + Hey + {% endsnippet %} + + {%- render hey -%} + LIQUID + expected = <<~OUTPUT + + Hey + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with_variable + template = <<~LIQUID.strip + {% snippet hey %} +

Today is {{ "hello" | capitalize }}

+ {% endsnippet %} + + {%- render hey -%} + LIQUID + expected = <<~OUTPUT + +

Today is Hello

+ OUTPUT + + assert_template_result(expected, template) + end + + def test_render_multiple_inline_snippets + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {% snippet banner %} + + Welcome to my store! + + {% endsnippet %} + + {%- render input -%} + {%- render banner -%} + LIQUID + expected = <<~OUTPUT + + + + + + + Welcome to my store! + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with_argument + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with_doc_tag + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with_evaluated_assign + template = <<~LIQUID.strip + {% snippet input %} +

{{ greeting }}

+ {% endsnippet %} + + {%- assign greeting = "hello" | upcase -%} + {%- render input, greeting: greeting -%} + LIQUID + expected = <<~OUTPUT + +

HELLO

+ OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with_multiple_arguments + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text", value: "Hello" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippets_using_same_argument_name + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {% snippet inputs %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + {%- render inputs, type: "password", value: "pass" -%} + LIQUID + + expected = <<~OUTPUT + + + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_empty_string_when_missing_argument + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_snippets_as_arguments + template = <<~LIQUID.strip + {% assign color_scheme = 'dark' %} + + {% snippet header %} +
+ {{ message }} +
+ {% endsnippet %} + + {% snippet main %} + {% assign color_scheme = 'auto' %} + +
+ {% render header, message: 'Welcome!' %} +
+ {% endsnippet %} + + {% render main, header: header %} + LIQUID + + expected = <<~OUTPUT + + + + + + + + + +
+ +
+ Welcome! +
+ +
+ OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_shouldnt_leak_context + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text", value: "Hello" -%} + + {{ type }} + {{ value }} + LIQUID + expected = <<~OUTPUT + + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_multiple_inline_snippets_without_leaking_context + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + {% enddoc %} + + + {% endsnippet %} + + {% snippet no_leak %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + {%- render no_leak -%} + LIQUID + expected = <<~OUTPUT + + + + + + + + + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_ignores_outside_context + template = <<~LIQUID.strip + {% assign color_scheme = 'dark' %} + + {% snippet header %} +
+ {{ message }} +
+ {% endsnippet %} + + + {% render header, message: 'Welcome!' %} + LIQUID + expected = <<~OUTPUT + + + + + + +
+ Welcome! +
+ OUTPUT + + assert_template_result(expected, template) + end + + def test_render_captured_snippet + template = <<~LIQUID + {% snippet header %} +
+ {{ message }} +
+ {% endsnippet %} + + {% capture up_header %} + {%- render header, message: 'Welcome!' -%} + {% endcapture %} + + {{ up_header | upcase }} + + {{ header | upcase }} + + {{ header }} + LIQUID + expected = <<~OUTPUT + + + + + +
+ WELCOME! +
+ + + SNIPPETDROP + + SnippetDrop + OUTPUT + + assert_template_result(expected, template) + end + + def test_inline_snippet_local_scope_takes_precedence + template = <<~LIQUID + {% assign color_scheme = 'dark' %} + + {% snippet header %} + {% assign color_scheme = 'light' %} +
+ {{ message }} +
+ {% endsnippet %} + + {{ color_scheme }} + + {% render header, message: 'Welcome!', color_scheme: color_scheme %} + + {{ color_scheme }} + LIQUID + expected = <<~OUTPUT + + + + + dark + + + +
+ Welcome! +
+ + + dark + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_forloop + template = <<~LIQUID.strip + {% snippet item %} +
  • {{ forloop.index }}: {{ item }}
  • + {% endsnippet %} + + {% assign items = "A,B,C" | split: "," %} + {%- render item for items -%} + LIQUID + expected = <<~OUTPUT + + + +
  • 1: A
  • + +
  • 2: B
  • + +
  • 3: C
  • + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_with + template = <<~LIQUID.strip + {% snippet header %} +
    {{ header }}
    + {% endsnippet %} + + {% assign product = "Apple" %} + {%- render header with product -%} + LIQUID + expected = <<~OUTPUT + + + +
    Apple
    + OUTPUT + + assert_template_result(expected, template) + end + + def test_render_inline_snippet_alias + template = <<~LIQUID.strip + {% snippet product_card %} +
    {{ item }}
    + {% endsnippet %} + + {% assign featured = "Apple" %} + {%- render product_card with featured as item -%} + LIQUID + expected = <<~OUTPUT + + + +
    Apple
    + OUTPUT + + assert_template_result(expected, template) + end + + def test_snippet_with_invalid_identifier + template = <<~LIQUID + {% snippet header foo bar %} + Invalid + {% endsnippet %} + LIQUID + + exception = assert_raises(SyntaxError) { Liquid::Template.parse(template) } + + assert_match("Expected end_of_string but found id", exception.message) + end + + def test_render_with_non_existent_tag + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true) + {% snippet foo %} + {% render non_existent %} + {% endsnippet %} + + {% render foo %} + LIQUID + + expected = <<~TEXT + + + + Liquid error (index line 2): internal + TEXT + template.name = "index" + + assert_equal(expected, template.render('errors' => ErrorDrop.new)) + end + + def test_render_handles_errors + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true) + {% snippet foo %} + {% render non_existent %} will raise an error. + + Bla bla test. + + This is an argument error: {{ 'test' | slice: 'not a number' }} + {% endsnippet %} + + {% render foo %} + LIQUID + + expected = <<~TEXT + + + + Liquid error (index line 2): internal will raise an error. + + Bla bla test. + + This is an argument error: Liquid error (index line 6): invalid integer + TEXT + template.name = "index" + + assert_equal(expected, template.render('errors' => ErrorDrop.new)) + end + end + + class RigidMode < SnippetTest + def test_valid_inline_snippet + template = <<~LIQUID.strip + {% snippet input %} + Hey + {% endsnippet %} + LIQUID + expected = '' + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet + template = <<~LIQUID.strip + {% snippet hey %} + Hey + {% endsnippet %} + + {%- render hey -%} + LIQUID + expected = <<~OUTPUT + + Hey + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with_variable + template = <<~LIQUID.strip + {% snippet hey %} +

    Today is {{ "hello" | capitalize }}

    + {% endsnippet %} + + {%- render hey -%} + LIQUID + expected = <<~OUTPUT + +

    Today is Hello

    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_multiple_inline_snippets + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {% snippet banner %} + + Welcome to my store! + + {% endsnippet %} + + {%- render input -%} + {%- render banner -%} + LIQUID + expected = <<~OUTPUT + + + + + + + Welcome to my store! + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with_argument + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with_doc_tag + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with_evaluated_assign + template = <<~LIQUID.strip + {% snippet input %} +

    {{ greeting }}

    + {% endsnippet %} + + {%- assign greeting = "hello" | upcase -%} + {%- render input, greeting: greeting -%} + LIQUID + expected = <<~OUTPUT + +

    HELLO

    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with_multiple_arguments + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text", value: "Hello" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippets_using_same_argument_name + template = <<~LIQUID.strip + {% snippet input %} + + {% endsnippet %} + + {% snippet inputs %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + {%- render inputs, type: "password", value: "pass" -%} + LIQUID + + expected = <<~OUTPUT + + + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_empty_string_when_missing_argument + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text" -%} + LIQUID + expected = <<~OUTPUT + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_snippets_as_arguments + template = <<~LIQUID.strip + {% assign color_scheme = 'dark' %} + + {% snippet header %} +
    + {{ message }} +
    + {% endsnippet %} + + {% snippet main %} + {% assign color_scheme = 'auto' %} + +
    + {% render header, message: 'Welcome!' %} +
    + {% endsnippet %} + + {% render main, header: header %} + LIQUID + + expected = <<~OUTPUT + + + + + + + + + +
    + +
    + Welcome! +
    + +
    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_shouldnt_leak_context + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + @param {string} value - Input value. + {% enddoc %} + + + {% endsnippet %} + + {%- render input, type: "text", value: "Hello" -%} + + {{ type }} + {{ value }} + LIQUID + expected = <<~OUTPUT + + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_multiple_inline_snippets_without_leaking_context + template = <<~LIQUID.strip + {% snippet input %} + {% doc %} + @param {string} type - Input type. + {% enddoc %} + + + {% endsnippet %} + + {% snippet no_leak %} + + {% endsnippet %} + + {%- render input, type: "text" -%} + {%- render no_leak -%} + LIQUID + expected = <<~OUTPUT + + + + + + + + + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_ignores_outside_context + template = <<~LIQUID.strip + {% assign color_scheme = 'dark' %} + + {% snippet header %} +
    + {{ message }} +
    + {% endsnippet %} + + + {% render header, message: 'Welcome!' %} + LIQUID + expected = <<~OUTPUT + + + + + + +
    + Welcome! +
    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_inline_snippet_local_scope_takes_precedence + template = <<~LIQUID + {% assign color_scheme = 'dark' %} + + {% snippet header %} + {% assign color_scheme = 'light' %} +
    + {{ message }} +
    + {% endsnippet %} + + {{ color_scheme }} + + {% render header, message: 'Welcome!', color_scheme: color_scheme %} + + {{ color_scheme }} + LIQUID + expected = <<~OUTPUT + + + + + dark + + + +
    + Welcome! +
    + + + dark + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_forloop + template = <<~LIQUID.strip + {% snippet item %} +
  • {{ forloop.index }}: {{ item }}
  • + {% endsnippet %} + + {% assign items = "A,B,C" | split: "," %} + {%- render item for items -%} + LIQUID + expected = <<~OUTPUT + + + +
  • 1: A
  • + +
  • 2: B
  • + +
  • 3: C
  • + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_with + template = <<~LIQUID.strip + {% snippet header %} +
    {{ header }}
    + {% endsnippet %} + + {% assign product = "Apple" %} + {%- render header with product -%} + LIQUID + expected = <<~OUTPUT + + + +
    Apple
    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_inline_snippet_alias + template = <<~LIQUID.strip + {% snippet product_card %} +
    {{ item }}
    + {% endsnippet %} + + {% assign featured = "Apple" %} + {%- render product_card with featured as item -%} + LIQUID + expected = <<~OUTPUT + + + +
    Apple
    + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_captured_snippet + template = <<~LIQUID + {% snippet header %} +
    + {{ message }} +
    + {% endsnippet %} + + {% capture up_header %} + {%- render header, message: 'Welcome!' -%} + {% endcapture %} + + {{ up_header | upcase }} + + {{ header | upcase }} + + {{ header }} + LIQUID + expected = <<~OUTPUT + + + + + +
    + WELCOME! +
    + + + SNIPPETDROP + + SnippetDrop + OUTPUT + + assert_template_result(expected, template, error_mode: :rigid) + end + + def test_render_with_invalid_identifier + template = "{% render 123 %}" + + exception = assert_raises(SyntaxError) do + Liquid::Template.parse(template, error_mode: :rigid) + end + + assert_match("Expected a string or identifier, found 123", exception.message) + end + + def test_render_with_non_existent_tag + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true, error_mode: :rigid) + {% snippet foo %} + {% render non_existent %} + {% endsnippet %} + + {% render foo %} + LIQUID + + expected = <<~TEXT + + + + Liquid error (index line 2): internal + TEXT + template.name = "index" + + assert_equal(expected, template.render('errors' => ErrorDrop.new)) + end + + def test_render_handles_errors + template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true, error_mode: :rigid) + {% snippet foo %} + {% render non_existent %} will raise an error. + + Bla bla test. + + This is an argument error: {{ 'test' | slice: 'not a number' }} + {% endsnippet %} + + {% render foo %} + LIQUID + + expected = <<~TEXT + + + + Liquid error (index line 2): internal will raise an error. + + Bla bla test. + + This is an argument error: Liquid error (index line 6): invalid integer + TEXT + template.name = "index" + + assert_equal(expected, template.render('errors' => ErrorDrop.new)) + end + + def test_render_with_no_identifier + template = "{% render %}" + + exception = assert_raises(SyntaxError) do + Liquid::Template.parse(template, error_mode: :rigid) + end + + assert_match("Expected a string or identifier, found nothing", exception.message) + end + + def test_snippet_with_invalid_identifier + template = <<~LIQUID + {% snippet header foo bar %} + Invalid + {% endsnippet %} + LIQUID + + exception = assert_raises(SyntaxError) do + Liquid::Template.parse(template, error_mode: :rigid) + end + + assert_match("Expected end_of_string but found id", exception.message) + end + end + + class ResourceLimits < SnippetTest + def test_increment_assign_score_by_bytes_not_characters + t = Template.parse("{% snippet foo %}すごい{% endsnippet %}") + t.render! + assert_equal(9, t.resource_limits.assign_score) + end + end +end