From 3574889b38d29f8d149de2caffec27e2b542e871 Mon Sep 17 00:00:00 2001 From: did Date: Wed, 9 Apr 2014 00:31:19 +0200 Subject: [PATCH 1/5] implementation/tests of the template inheritance mechanism similar to what offers Django --- lib/liquid/tags/extends.rb | 78 +++++++++++++++++ lib/liquid/tags/inherited_block.rb | 102 ++++++++++++++++++++++ test/integration/tags/extends_tag_test.rb | 82 +++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 lib/liquid/tags/extends.rb create mode 100644 lib/liquid/tags/inherited_block.rb create mode 100644 test/integration/tags/extends_tag_test.rb diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb new file mode 100644 index 000000000..7d9e7fc79 --- /dev/null +++ b/lib/liquid/tags/extends.rb @@ -0,0 +1,78 @@ +module Liquid + + # Extends allows designer to use template inheritance + # + # {% extends home %} + # {% block content }Hello world{% endblock %} + # + class Extends < Block + Syntax = /(#{QuotedFragment}+)/ + + def initialize(tag_name, markup, options) + super + + if markup =~ Syntax + @template_name = $1.gsub(/["']/o, '').strip + else + raise(SyntaxError.new(options[:locale].t("errors.syntax.extends".freeze))) + end + end + + def parse(tokens) + # get the nodes of the template the object inherits from + parent_template = parse_parent_template + + # find the blocks in case there is a call to + # the super method inside the current template + @options.merge!(:blocks => self.find_blocks(parent_template.root.nodelist)) + + # finally, process the rest of the tokens + # the tags/blocks other than the InheritedBlock type will be ignored. + super + + # replace the nodes of the current template by the ones from the parent + @nodelist = parent_template.root.nodelist.clone + end + + def blank? + false + end + + protected + + def find_blocks(nodelist, blocks = {}) + if nodelist && nodelist.any? + 0.upto(nodelist.size - 1).each do |index| + node = nodelist[index] + + # is the node an inherited block? + if node.respond_to?(:call_super) + new_node = node.class.clone_block(node) + + nodelist.insert(index, new_node) + nodelist.delete_at(index + 1) + + blocks[node.name] = new_node + end + if node.respond_to?(:nodelist) + # find nested blocks too + self.find_blocks(node.nodelist, blocks) + end + end + end + blocks + end + + private + + def parse_parent_template + source = Template.file_system.read_template_file(@template_name, {}) + Template.parse(source) + end + + def assert_missing_delimitation! + end + end + + Template.register_tag('extends', Extends) +end \ No newline at end of file diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb new file mode 100644 index 000000000..e4b415733 --- /dev/null +++ b/lib/liquid/tags/inherited_block.rb @@ -0,0 +1,102 @@ +module Liquid + + # Blocks are used with the Extends tag to define + # the content of blocks. Nested blocks are allowed. + # + # {% extends home %} + # {% block content }Hello world{% endblock %} + # + class InheritedBlock < Block + Syntax = /(#{QuotedFragment}+)/ + + attr_accessor :parent + attr_accessor :nodelist + attr_reader :name + + def initialize(tag_name, markup, options) + super + + if markup =~ Syntax + @name = $1.gsub(/["']/o, '').strip + else + raise(SyntaxError.new(options[:locale].t("errors.syntax.block")), options[:line]) + end + + self.set_full_name!(options) + + (options[:block_stack] ||= []).push(self) + options[:current_block] = self + end + + def render(context) + context.stack do + context['block'] = InheritedBlockDrop.new(self) + render_all(@nodelist, context) + end + end + + def end_tag + self.register_current_block + + options[:block_stack].pop + options[:current_block] = options[:block_stack].last + end + + def call_super(context) + if parent + parent.render(context) + else + '' + end + end + + def self.clone_block(block) + new_block = new(block.send(:instance_variable_get, :"@tag_name"), block.name, {}) + new_block.parent = block.parent + new_block.nodelist = block.nodelist + new_block + end + + protected + + def set_full_name!(options) + if options[:current_block] + @name = options[:current_block].name + '/' + @name + end + end + + def register_current_block + options[:blocks] ||= {} + + block = options[:blocks][@name] + + if block + # copy the existing block in order to make it a parent of the parsed block + new_block = self.class.clone_block(block) + + # replace the up-to-date version of the block in the parent template + block.parent = new_block + block.nodelist = @nodelist + end + end + + end + + class InheritedBlockDrop < Drop + + def initialize(block) + @block = block + end + + def name + @block.name + end + + def super + @block.call_super(@context) + end + + end + + Template.register_tag('block', InheritedBlock) +end \ No newline at end of file diff --git a/test/integration/tags/extends_tag_test.rb b/test/integration/tags/extends_tag_test.rb new file mode 100644 index 000000000..c13570669 --- /dev/null +++ b/test/integration/tags/extends_tag_test.rb @@ -0,0 +1,82 @@ +require 'test_helper' + +class LayoutFileSystem + def read_template_file(template_path, context) + case template_path + when "base" + "base" + + when "inherited" + "{% extends base %}" + + when "page_with_title" + "

{% block title %}Hello{% endblock %}

Lorem ipsum

" + + when "product" + "

Our product: {{ name }}

{% block info %}{% endblock %}" + + when "product_with_warranty" + "{% extends product %}{% block info %}

mandatory warranty

{% endblock %}" + + when "product_with_static_price" + "{% extends product %}{% block info %}

Some info

{% block price %}

$42.00

{% endblock %}{% endblock %}" + + else + template_path + end + end +end + +class ExtendsTagTest < Test::Unit::TestCase + include Liquid + + def setup + Liquid::Template.file_system = LayoutFileSystem.new + end + + def test_template_extends_another_template + assert_template_result "base", + "{% extends base %}" + end + + def test_template_extends_an_inherited_template + assert_template_result "base", + "{% extends inherited %}" + end + + def test_template_can_pass_variables_to_the_parent_template + assert_template_result "

Our product: Macbook

", + "{% extends product %}", 'name' => 'Macbook' + end + + def test_template_can_pass_variables_to_the_inherited_parent_template + assert_template_result "

Our product: PC

mandatory warranty

", + "{% extends product_with_warranty %}", 'name' => 'PC' + end + + def test_template_does_not_render_statements_outside_blocks + assert_template_result "base", + "{% extends base %} Hello world" + end + + def test_template_extends_another_template_with_a_single_block + assert_template_result "

Hello

Lorem ipsum

", + "{% extends page_with_title %}" + end + + def test_template_overrides_a_block + assert_template_result "

Sweet

Lorem ipsum

", + "{% extends page_with_title %}{% block title %}Sweet{% endblock %}" + end + + def test_template_has_access_to_the_content_of_the_overriden_block + assert_template_result "

Hello world

Lorem ipsum

", + "{% extends page_with_title %}{% block title %}{{ block.super }} world{% endblock %}" + end + + def test_template_accepts_nested_blocks + assert_template_result "

Our product: iPhone

Some info

$42.00

(not on sale)

", + "{% extends product_with_static_price %}{% block info/price %}{{ block.super }}

(not on sale)

{% endblock %}", 'name' => 'iPhone' + end + +end # ExtendsTagTest From 3f0f76aec116812f4d18e816df7193190044c617 Mon Sep 17 00:00:00 2001 From: did Date: Wed, 9 Apr 2014 11:32:41 +0200 Subject: [PATCH 2/5] apply the comments about the code made by @fw42 --- lib/liquid.rb | 2 +- lib/liquid/drops/inherited_block_drop.rb | 24 +++++++++++ lib/liquid/tags/extends.rb | 8 ++-- lib/liquid/tags/inherited_block.rb | 52 ++++++++++-------------- 4 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 lib/liquid/drops/inherited_block_drop.rb diff --git a/lib/liquid.rb b/lib/liquid.rb index 484f8b689..1a153d821 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -63,4 +63,4 @@ module Liquid # Load all the tags of the standard library # -Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } +Dir[File.dirname(__FILE__) + '/liquid/{tags,drops}/*.rb'].each { |f| require f } diff --git a/lib/liquid/drops/inherited_block_drop.rb b/lib/liquid/drops/inherited_block_drop.rb new file mode 100644 index 000000000..e00a0fb45 --- /dev/null +++ b/lib/liquid/drops/inherited_block_drop.rb @@ -0,0 +1,24 @@ +module Liquid + + # Used to render the content of the parent block. + # + # {% extends home %} + # {% block content }{{ block.super }}{% endblock %} + # + class InheritedBlockDrop < Drop + + def initialize(block) + @block = block + end + + def name + @block.name + end + + def super + @block.call_super(@context) + end + + end + +end \ No newline at end of file diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 7d9e7fc79..b181160fd 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -41,13 +41,11 @@ def blank? protected def find_blocks(nodelist, blocks = {}) - if nodelist && nodelist.any? - 0.upto(nodelist.size - 1).each do |index| - node = nodelist[index] - + if nodelist + nodelist.each_with_index do |node, index| # is the node an inherited block? if node.respond_to?(:call_super) - new_node = node.class.clone_block(node) + new_node = node.clone_it nodelist.insert(index, new_node) nodelist.delete_at(index + 1) diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index e4b415733..9aa9b34d2 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -9,9 +9,7 @@ module Liquid class InheritedBlock < Block Syntax = /(#{QuotedFragment}+)/ - attr_accessor :parent - attr_accessor :nodelist - attr_reader :name + attr_reader :name, :parent def initialize(tag_name, markup, options) super @@ -22,7 +20,7 @@ def initialize(tag_name, markup, options) raise(SyntaxError.new(options[:locale].t("errors.syntax.block")), options[:line]) end - self.set_full_name!(options) + set_full_name!(options) (options[:block_stack] ||= []).push(self) options[:current_block] = self @@ -36,8 +34,9 @@ def render(context) end def end_tag - self.register_current_block + link_it_with_ancestor + # clean the stack options[:block_stack].pop options[:current_block] = options[:block_stack].last end @@ -50,14 +49,22 @@ def call_super(context) end end - def self.clone_block(block) - new_block = new(block.send(:instance_variable_get, :"@tag_name"), block.name, {}) - new_block.parent = block.parent - new_block.nodelist = block.nodelist + def clone_it + self.class.clone_it(self) + end + + def attach_parent(parent, nodelist) + @parent = parent + @nodelist = nodelist + end + + def self.clone_it(block) + new_block = new(block.block_name, block.name, {}) + new_block.attach_parent(block.parent, block.nodelist) new_block end - protected + private def set_full_name!(options) if options[:current_block] @@ -65,38 +72,21 @@ def set_full_name!(options) end end - def register_current_block + def link_it_with_ancestor options[:blocks] ||= {} block = options[:blocks][@name] if block - # copy the existing block in order to make it a parent of the parsed block - new_block = self.class.clone_block(block) + # copy/clone the existing block in order to make it a parent of the parsed block + cloned_block = block.clone_it # replace the up-to-date version of the block in the parent template - block.parent = new_block - block.nodelist = @nodelist + block.attach_parent(cloned_block, @nodelist) end end end - class InheritedBlockDrop < Drop - - def initialize(block) - @block = block - end - - def name - @block.name - end - - def super - @block.call_super(@context) - end - - end - Template.register_tag('block', InheritedBlock) end \ No newline at end of file From a2c37667028475e0b0fd59230af824c5d8e3f2e3 Mon Sep 17 00:00:00 2001 From: did Date: Wed, 9 Apr 2014 17:21:40 +0200 Subject: [PATCH 3/5] regexp memoization for the extends and block tags --- lib/liquid/tags/extends.rb | 2 +- lib/liquid/tags/inherited_block.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index b181160fd..8a728d24d 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -6,7 +6,7 @@ module Liquid # {% block content }Hello world{% endblock %} # class Extends < Block - Syntax = /(#{QuotedFragment}+)/ + Syntax = /(#{QuotedFragment}+)/o def initialize(tag_name, markup, options) super diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 9aa9b34d2..d58a98ce2 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -7,7 +7,7 @@ module Liquid # {% block content }Hello world{% endblock %} # class InheritedBlock < Block - Syntax = /(#{QuotedFragment}+)/ + Syntax = /(#{QuotedFragment}+)/o attr_reader :name, :parent From 7d14d7e00bde203e61c473f144590f5998df2be8 Mon Sep 17 00:00:00 2001 From: did Date: Tue, 15 Apr 2014 11:37:53 +0200 Subject: [PATCH 4/5] apply changes from @phoet comments --- lib/liquid/tags/extends.rb | 5 ++--- lib/liquid/tags/inherited_block.rb | 10 ++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 8a728d24d..dc929902b 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -47,8 +47,7 @@ def find_blocks(nodelist, blocks = {}) if node.respond_to?(:call_super) new_node = node.clone_it - nodelist.insert(index, new_node) - nodelist.delete_at(index + 1) + nodelist[index] = new_node blocks[node.name] = new_node end @@ -73,4 +72,4 @@ def assert_missing_delimitation! end Template.register_tag('extends', Extends) -end \ No newline at end of file +end diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index d58a98ce2..c9096c4c6 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -20,7 +20,7 @@ def initialize(tag_name, markup, options) raise(SyntaxError.new(options[:locale].t("errors.syntax.block")), options[:line]) end - set_full_name!(options) + @name = "#{options[:current_block].name}/#{@name}" if options[:current_block] (options[:block_stack] ||= []).push(self) options[:current_block] = self @@ -66,12 +66,6 @@ def self.clone_it(block) private - def set_full_name!(options) - if options[:current_block] - @name = options[:current_block].name + '/' + @name - end - end - def link_it_with_ancestor options[:blocks] ||= {} @@ -89,4 +83,4 @@ def link_it_with_ancestor end Template.register_tag('block', InheritedBlock) -end \ No newline at end of file +end From 7e4eacddda241bce279e2029ffaa0e8f7c5086f0 Mon Sep 17 00:00:00 2001 From: did Date: Sun, 18 Jan 2015 16:03:08 +0100 Subject: [PATCH 5/5] template inheritance works with liquid 3.0 --- lib/liquid/tags/extends.rb | 54 +++++-------- lib/liquid/tags/inherited_block.rb | 95 +++++++++++++---------- test/integration/tags/extends_tag_test.rb | 24 +++++- 3 files changed, 98 insertions(+), 75 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index dc929902b..b8e37f00d 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -1,6 +1,6 @@ module Liquid - # Extends allows designer to use template inheritance + # Extends allows designer to use template inheritance. # # {% extends home %} # {% block content }Hello world{% endblock %} @@ -16,22 +16,23 @@ def initialize(tag_name, markup, options) else raise(SyntaxError.new(options[:locale].t("errors.syntax.extends".freeze))) end + + # variables needed by the inheritance mechanism during the parsing + options[:inherited_blocks] ||= { + nested: [], # used to get the full name of the blocks if nested (stack mechanism) + all: {} # keep track of the blocks by their full name + } end def parse(tokens) - # get the nodes of the template the object inherits from - parent_template = parse_parent_template - - # find the blocks in case there is a call to - # the super method inside the current template - @options.merge!(:blocks => self.find_blocks(parent_template.root.nodelist)) - - # finally, process the rest of the tokens - # the tags/blocks other than the InheritedBlock type will be ignored. super - # replace the nodes of the current template by the ones from the parent - @nodelist = parent_template.root.nodelist.clone + parent_template = parse_parent_template + + # replace the nodes of the current template by those from the parent + # which itself may have have done the same operation if it includes + # the extends tag. + nodelist.replace(parent_template.root.nodelist) end def blank? @@ -40,35 +41,22 @@ def blank? protected - def find_blocks(nodelist, blocks = {}) - if nodelist - nodelist.each_with_index do |node, index| - # is the node an inherited block? - if node.respond_to?(:call_super) - new_node = node.clone_it - - nodelist[index] = new_node + def parse_body(body, tokens) + body.parse(tokens, options) do |end_tag_name, end_tag_params| + @blank &&= body.blank? - blocks[node.name] = new_node - end - if node.respond_to?(:nodelist) - # find nested blocks too - self.find_blocks(node.nodelist, blocks) - end - end + # Note: extends does not require the "end tag". + return false if end_tag_name.nil? end - blocks - end - private + true + end def parse_parent_template source = Template.file_system.read_template_file(@template_name, {}) - Template.parse(source) + Template.parse(source, options) end - def assert_missing_delimitation! - end end Template.register_tag('extends', Extends) diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index c9096c4c6..9b2284f32 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -9,7 +9,11 @@ module Liquid class InheritedBlock < Block Syntax = /(#{QuotedFragment}+)/o - attr_reader :name, :parent + attr_reader :name + + # linked chain of inherited blocks included + # in different templates if multiple extends + attr_accessor :parent, :descendant def initialize(tag_name, markup, options) super @@ -20,63 +24,72 @@ def initialize(tag_name, markup, options) raise(SyntaxError.new(options[:locale].t("errors.syntax.block")), options[:line]) end - @name = "#{options[:current_block].name}/#{@name}" if options[:current_block] - - (options[:block_stack] ||= []).push(self) - options[:current_block] = self + prepare_for_inheritance end - def render(context) - context.stack do - context['block'] = InheritedBlockDrop.new(self) - render_all(@nodelist, context) + def prepare_for_inheritance + # give a different name if this is a nested block + if block = options[:inherited_blocks][:nested].last + @name = "#{block.name}/#{@name}" end - end - def end_tag - link_it_with_ancestor + # append this block to the stack in order to + # get a name for the other nested inherited blocks + options[:inherited_blocks][:nested].push(self) - # clean the stack - options[:block_stack].pop - options[:current_block] = options[:block_stack].last - end + # build the linked chain of inherited blocks + # make a link with the descendant and the parent (chained list) + if descendant = options[:inherited_blocks][:all][@name] + self.descendant = descendant + descendant.parent = self - def call_super(context) - if parent - parent.render(context) - else - '' + # get the value of the blank property from the descendant + @blank = descendant.blank? #false end - end - def clone_it - self.class.clone_it(self) + # become the descendant of the inherited block from the parent template + options[:inherited_blocks][:all][@name] = self end - def attach_parent(parent, nodelist) - @parent = parent - @nodelist = nodelist - end + def parse(tokens) + super - def self.clone_it(block) - new_block = new(block.block_name, block.name, {}) - new_block.attach_parent(block.parent, block.nodelist) - new_block + # when the parsing of the block is done, we can then remove it from the stack + options[:inherited_blocks][:nested].pop end - private + alias_method :render_without_inheritance, :render - def link_it_with_ancestor - options[:blocks] ||= {} + def render(context) + context.stack do + # look for the very first descendant + block = self_or_first_descendant - block = options[:blocks][@name] + if block != self + # the block drop is in charge of rendering "{{ block.super }}" + context['block'] = InheritedBlockDrop.new(block) + end - if block - # copy/clone the existing block in order to make it a parent of the parsed block - cloned_block = block.clone_it + block.render_without_inheritance(context) + end + end + + # when we render an inherited block, we need the version of the + # very first descendant. + def self_or_first_descendant + block = self + while block.descendant; block = block.descendant; end + block + end - # replace the up-to-date version of the block in the parent template - block.attach_parent(cloned_block, @nodelist) + def call_super(context) + if parent + # remove the block from the linked chain + parent.descendant = nil + + parent.render(context) + else + '' end end diff --git a/test/integration/tags/extends_tag_test.rb b/test/integration/tags/extends_tag_test.rb index c13570669..fdfc7a1d2 100644 --- a/test/integration/tags/extends_tag_test.rb +++ b/test/integration/tags/extends_tag_test.rb @@ -27,7 +27,7 @@ def read_template_file(template_path, context) end end -class ExtendsTagTest < Test::Unit::TestCase +class ExtendsTagTest < Minitest::Test include Liquid def setup @@ -79,4 +79,26 @@ def test_template_accepts_nested_blocks "{% extends product_with_static_price %}{% block info/price %}{{ block.super }}

(not on sale)

{% endblock %}", 'name' => 'iPhone' end + # def _print(node, depth = 0) + # offset = ('.' * depth) + ' ' + + # if node.respond_to?(:template_name) # extends + # puts "#{offset}Extends #{node.template_name}" + # elsif node.respond_to?(:push_to_stack) # inherited block + # puts "#{offset}Block #{node.name} (descendant: #{(!node.descendant.nil?).inspect} / parent: #{(!node.parent.nil?).inspect}), nodes? #{node.self_or_first_ascendant.nodelist.size.inspect} / #{node.blank?.inspect}" + # elsif node.respond_to?(:name) + # puts "#{offset}#{node.name}" + # else + # puts "#{offset} #{node}" + # end + + # if node.respond_to?(:nodelist) + # _node = node.respond_to?(:self_or_first_ascendant) ? node.self_or_first_ascendant : node + + # _node.nodelist.each_with_index do |__node, index| + # print(__node, depth + 1) + # end + # end + # end + end # ExtendsTagTest