From ae809b6290642353c1df7064800952fff44a3503 Mon Sep 17 00:00:00 2001 From: Mike Angell Date: Mon, 7 Oct 2019 14:10:12 +1100 Subject: [PATCH] Add support for alias --- lib/liquid/tags/include.rb | 8 +- lib/liquid/tags/render.rb | 39 ++++++-- test/integration/tags/include_tag_test.rb | 103 +++++++++++---------- test/integration/tags/render_tag_test.rb | 104 ++++++++++++++++------ 4 files changed, 172 insertions(+), 82 deletions(-) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index bbcfb1c..3f4177f 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -16,18 +16,20 @@ module Liquid # {% include 'product' for products %} # class Include < Tag - Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o + SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSignature}+))?/o + Syntax = SYNTAX attr_reader :template_name_expr, :variable_name_expr, :attributes def initialize(tag_name, markup, options) super - if markup =~ Syntax + if markup =~ SYNTAX template_name = Regexp.last_match(1) variable_name = Regexp.last_match(3) + @alias_name = Regexp.last_match(5) || nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @template_name_expr = Expression.parse(template_name) @attributes = {} @@ -54,7 +56,7 @@ module Liquid parse_context: parse_context ) - context_variable_name = template_name.split('/').last + context_variable_name = @alias_name ? @alias_name : template_name.split('/').last variable = if @variable_name_expr context.evaluate(@variable_name_expr) diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb index 1403b58..c4c33ff 100644 --- a/lib/liquid/tags/render.rb +++ b/lib/liquid/tags/render.rb @@ -2,7 +2,7 @@ module Liquid class Render < Tag - SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o + SYNTAX = /(#{QuotedString}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSignature}+))?/o disable_tags "include" @@ -14,7 +14,10 @@ module Liquid raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX template_name = Regexp.last_match(1) + variable_name = Regexp.last_match(3) + @alias_name = Regexp.last_match(5) || nil + @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @template_name_expr = Expression.parse(template_name) @attributes = {} @@ -38,13 +41,35 @@ module Liquid parse_context: parse_context ) - inner_context = context.new_isolated_subcontext - inner_context.template_name = template_name - inner_context.partial = true - @attributes.each do |key, value| - inner_context[key] = context.evaluate(value) + context_variable_name = @alias_name ? @alias_name : "this" + + variable = if @variable_name_expr + context.evaluate(@variable_name_expr) + else + context.find_variable(template_name, raise_on_not_found: false) + end + + if variable.is_a?(Array) + variable.each do |var| + inner_context = context.new_isolated_subcontext + inner_context.template_name = template_name + inner_context.partial = true + @attributes.each do |key, value| + inner_context[key] = context.evaluate(value) + end + inner_context[context_variable_name] = var + partial.render_to_output_buffer(inner_context, output) + end + else + inner_context = context.new_isolated_subcontext + inner_context.template_name = template_name + inner_context.partial = true + @attributes.each do |key, value| + inner_context[key] = context.evaluate(value) + end + inner_context[context_variable_name] = variable + partial.render_to_output_buffer(inner_context, output) end - partial.render_to_output_buffer(inner_context, output) output end diff --git a/test/integration/tags/include_tag_test.rb b/test/integration/tags/include_tag_test.rb index 45410a7..c0400a0 100644 --- a/test/integration/tags/include_tag_test.rb +++ b/test/integration/tags/include_tag_test.rb @@ -8,6 +8,9 @@ class TestFileSystem when "product" "Product: {{ product.title }} " + when "product_alias" + "Product: {{ product.title }} " + when "locale_variables" "Locale: {{echo1}} {{echo2}}" @@ -82,56 +85,66 @@ class IncludeTagTest < Minitest::Test end def test_include_tag_looks_for_file_system_in_registers_first - assert_equal 'from OtherFileSystem', - Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }) + assert_equal('from OtherFileSystem', + Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new })) end def test_include_tag_with - assert_template_result "Product: Draft 151cm ", - "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] + assert_template_result("Product: Draft 151cm ", + "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) + end + + def test_include_tag_with_alias + assert_template_result("Product: Draft 151cm ", + "{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) + end + + def test_include_tag_for_alias + assert_template_result("Product: Draft 151cm Product: Element 155cm ", + "{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_with_default_name - assert_template_result "Product: Draft 151cm ", - "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' } + assert_template_result("Product: Draft 151cm ", + "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' }) end def test_include_tag_for - assert_template_result "Product: Draft 151cm Product: Element 155cm ", - "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] + assert_template_result("Product: Draft 151cm Product: Element 155cm ", + "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_with_local_variables - assert_template_result "Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}" + assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}") end def test_include_tag_with_multiple_local_variables - assert_template_result "Locale: test123 test321", - "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}" + assert_template_result("Locale: test123 test321", + "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}") end def test_include_tag_with_multiple_local_variables_from_context - assert_template_result "Locale: test123 test321", + assert_template_result("Locale: test123 test321", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", - 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } + 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' }) end def test_included_templates_assigns_variables - assert_template_result "bar", "{% include 'assignments' %}{{ foo }}" + assert_template_result("bar", "{% include 'assignments' %}{{ foo }}") end def test_nested_include_tag - assert_template_result "body body_detail", "{% include 'body' %}" + assert_template_result("body body_detail", "{% include 'body' %}") - assert_template_result "header body body_detail footer", "{% include 'nested_template' %}" + assert_template_result("header body body_detail footer", "{% include 'nested_template' %}") end def test_nested_include_with_variable - assert_template_result "Product: Draft 151cm details ", - "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' } + assert_template_result("Product: Draft 151cm details ", + "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' }) - assert_template_result "Product: Draft 151cm details Product: Element 155cm details ", - "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] + assert_template_result("Product: Draft 151cm details Product: Element 155cm details ", + "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }]) end def test_recursively_included_template_does_not_produce_endless_loop @@ -149,41 +162,41 @@ class IncludeTagTest < Minitest::Test end def test_dynamically_choosen_template - assert_template_result "Test123", "{% include template %}", "template" => 'Test123' - assert_template_result "Test321", "{% include template %}", "template" => 'Test321' + assert_template_result("Test123", "{% include template %}", "template" => 'Test123') + assert_template_result("Test321", "{% include template %}", "template" => 'Test321') - assert_template_result "Product: Draft 151cm ", "{% include template for product %}", - "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } + assert_template_result("Product: Draft 151cm ", "{% include template for product %}", + "template" => 'product', 'product' => { 'title' => 'Draft 151cm' }) end def test_include_tag_caches_second_read_of_same_partial file_system = CountingFileSystem.new - assert_equal 'from CountingFileSystemfrom CountingFileSystem', - Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) - assert_equal 1, file_system.count + assert_equal('from CountingFileSystemfrom CountingFileSystem', + Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) + assert_equal(1, file_system.count) end def test_include_tag_doesnt_cache_partials_across_renders file_system = CountingFileSystem.new - assert_equal 'from CountingFileSystem', - Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) - assert_equal 1, file_system.count + assert_equal('from CountingFileSystem', + Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) + assert_equal(1, file_system.count) - assert_equal 'from CountingFileSystem', - Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) - assert_equal 2, file_system.count + assert_equal('from CountingFileSystem', + Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) + assert_equal(2, file_system.count) end def test_include_tag_within_if_statement - assert_template_result "foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}" + assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}") end def test_custom_include_tag original_tag = Liquid::Template.tags['include'] Liquid::Template.tags['include'] = CustomInclude begin - assert_equal "custom_foo", - Template.parse("{% include 'custom_foo' %}").render! + assert_equal("custom_foo", + Template.parse("{% include 'custom_foo' %}").render!) ensure Liquid::Template.tags['include'] = original_tag end @@ -193,8 +206,8 @@ class IncludeTagTest < Minitest::Test original_tag = Liquid::Template.tags['include'] Liquid::Template.tags['include'] = CustomInclude begin - assert_equal "custom_foo_if_true", - Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render! + assert_equal("custom_foo_if_true", + Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!) ensure Liquid::Template.tags['include'] = original_tag end @@ -205,7 +218,7 @@ class IncludeTagTest < Minitest::Test a = Liquid::Template.parse(' {% include "nested_template" %}') a.render! - assert_empty a.errors + assert_empty(a.errors) end def test_passing_options_to_included_templates @@ -235,22 +248,22 @@ class IncludeTagTest < Minitest::Test end def test_including_via_variable_value - assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}" + assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}") - assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' } + assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' }) - assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } + assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }) end def test_including_with_strict_variables template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template.render(nil, strict_variables: true) - assert_equal [], template.errors + assert_equal([], template.errors) end def test_break_through_include - assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}" - assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}" + assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}") + assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}") end end # IncludeTagTest diff --git a/test/integration/tags/render_tag_test.rb b/test/integration/tags/render_tag_test.rb index 87373a2..117ecb8 100644 --- a/test/integration/tags/render_tag_test.rb +++ b/test/integration/tags/render_tag_test.rb @@ -7,39 +7,39 @@ class RenderTagTest < Minitest::Test def test_render_with_no_arguments Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content') - assert_template_result 'rendered content', '{% render "source" %}' + assert_template_result('rendered content', '{% render "source" %}') end def test_render_tag_looks_for_file_system_in_registers_first file_system = StubFileSystem.new('pick_a_source' => 'from register file system') - assert_equal 'from register file system', - Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }) + assert_equal('from register file system', + Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })) end def test_render_passes_named_arguments_into_inner_scope Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}') - assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}', - 'outer_product' => { 'title' => 'My Product' } + assert_template_result('My Product', '{% render "product", inner_product: outer_product %}', + 'outer_product' => { 'title' => 'My Product' }) end def test_render_accepts_literals_as_arguments Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}') - assert_template_result '123', '{% render "snippet", price: 123 %}' + assert_template_result('123', '{% render "snippet", price: 123 %}') end def test_render_accepts_multiple_named_arguments Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}') - assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}' + assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}') end def test_render_does_not_inherit_parent_scope_variables Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}') - assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}' + assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}') end def test_render_does_not_inherit_variable_with_same_name_as_snippet Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}') - assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}" + assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}") end def test_render_sets_the_correct_template_name_for_errors @@ -70,7 +70,7 @@ class RenderTagTest < Minitest::Test def test_render_does_not_mutate_parent_scope Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}') - assert_template_result '', "{% render 'snippet' %}{{ inner }}" + assert_template_result('', "{% render 'snippet' %}{{ inner }}") end def test_nested_render_tag @@ -78,7 +78,7 @@ class RenderTagTest < Minitest::Test 'one' => "one {% render 'two' %}", 'two' => 'two' ) - assert_template_result 'one two', "{% render 'one' %}" + assert_template_result('one two', "{% render 'one' %}") end def test_recursively_rendered_template_does_not_produce_endless_loop @@ -108,43 +108,43 @@ class RenderTagTest < Minitest::Test def test_include_tag_caches_second_read_of_same_partial file_system = StubFileSystem.new('snippet' => 'echo') - assert_equal 'echoecho', + assert_equal('echoecho', Template.parse('{% render "snippet" %}{% render "snippet" %}') - .render!({}, registers: { file_system: file_system }) - assert_equal 1, file_system.file_read_count + .render!({}, registers: { file_system: file_system })) + assert_equal(1, file_system.file_read_count) end def test_render_tag_doesnt_cache_partials_across_renders file_system = StubFileSystem.new('snippet' => 'my message') - assert_equal 'my message', - Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) - assert_equal 1, file_system.file_read_count + assert_equal('my message', + Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })) + assert_equal(1, file_system.file_read_count) - assert_equal 'my message', - Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) - assert_equal 2, file_system.file_read_count + assert_equal('my message', + Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })) + assert_equal(2, file_system.file_read_count) end def test_render_tag_within_if_statement Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message') - assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}' + assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}') end def test_break_through_render Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}') - assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}' - assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}' + assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}') + assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}') end def test_increment_is_isolated_between_renders Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}') - assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}' + assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}') end def test_decrement_is_isolated_between_renders Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}') - assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}' + assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}') end def test_includes_will_not_render_inside_render_tag @@ -153,7 +153,7 @@ class RenderTagTest < Minitest::Test 'test_include' => '{% include "foo" %}' ) - assert_template_result 'include usage is not allowed in this context', '{% render "test_include" %}' + assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}') end def test_includes_will_not_render_inside_nested_sibling_tags @@ -163,6 +163,56 @@ class RenderTagTest < Minitest::Test 'test_include' => '{% include "foo" %}' ) - assert_template_result 'include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}' + assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}') + end + + def test_include_tag_with + Liquid::Template.file_system = StubFileSystem.new( + 'product' => "Product: {{ this.title }} ", + 'product_alias' => "Product: {{ this.title }} ", + ) + + assert_template_result("Product: Draft 151cm ", + "{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) + end + + def test_include_tag_with_alias + Liquid::Template.file_system = StubFileSystem.new( + 'product' => "Product: {{ product.title }} ", + 'product_alias' => "Product: {{ product.title }} ", + ) + + assert_template_result("Product: Draft 151cm ", + "{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) + end + + def test_include_tag_for_alias + Liquid::Template.file_system = StubFileSystem.new( + 'product' => "Product: {{ product.title }} ", + 'product_alias' => "Product: {{ product.title }} ", + ) + + assert_template_result("Product: Draft 151cm Product: Element 155cm ", + "{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) + end + + def test_include_tag_with_default_name + Liquid::Template.file_system = StubFileSystem.new( + 'product' => "Product: {{ this.title }} ", + 'product_alias' => "Product: {{ this.title }} ", + ) + + assert_template_result("Product: Draft 151cm ", + "{% render 'product' %}", "product" => { 'title' => 'Draft 151cm' }) + end + + def test_include_tag_for + Liquid::Template.file_system = StubFileSystem.new( + 'product' => "Product: {{ this.title }} ", + 'product_alias' => "Product: {{ this.title }} ", + ) + + assert_template_result("Product: Draft 151cm Product: Element 155cm ", + "{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end end