Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implementation/tests of the template inheritance mechanism #336

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,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 }
24 changes: 24 additions & 0 deletions lib/liquid/drops/inherited_block_drop.rb
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions lib/liquid/tags/extends.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Liquid

# Extends allows designer to use template inheritance.
#
# {% extends home %}
# {% block content }Hello world{% endblock %}
#
class Extends < Block
Syntax = /(#{QuotedFragment}+)/o

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

# 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)
super

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?
false
end

protected

def parse_body(body, tokens)
body.parse(tokens, options) do |end_tag_name, end_tag_params|
@blank &&= body.blank?

# Note: extends does not require the "end tag".
return false if end_tag_name.nil?
end

true
end

def parse_parent_template
source = Template.file_system.read_template_file(@template_name, {})
Template.parse(source, options)
end

end

Template.register_tag('extends', Extends)
end
99 changes: 99 additions & 0 deletions lib/liquid/tags/inherited_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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}+)/o
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be SYNTAX, why do we use that style everywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea :-) I just followed the pattern of the other existing tags.


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

if markup =~ Syntax
@name = $1.gsub(/["']/o, '').strip
else
raise(SyntaxError.new(options[:locale].t("errors.syntax.block")), options[:line])
end

prepare_for_inheritance
end

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

# append this block to the stack in order to
# get a name for the other nested inherited blocks
options[:inherited_blocks][:nested].push(self)

# 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

# get the value of the blank property from the descendant
@blank = descendant.blank? #false
end

# become the descendant of the inherited block from the parent template
options[:inherited_blocks][:all][@name] = self
end

def parse(tokens)
super

# when the parsing of the block is done, we can then remove it from the stack
options[:inherited_blocks][:nested].pop
end

alias_method :render_without_inheritance, :render

def render(context)
context.stack do
# look for the very first descendant
block = self_or_first_descendant

if block != self
# the block drop is in charge of rendering "{{ block.super }}"
context['block'] = InheritedBlockDrop.new(block)
end

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

def call_super(context)
if parent
# remove the block from the linked chain
parent.descendant = nil

parent.render(context)
else
''
end
end

end

Template.register_tag('block', InheritedBlock)
end
104 changes: 104 additions & 0 deletions test/integration/tags/extends_tag_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
require 'test_helper'

class LayoutFileSystem
def read_template_file(template_path, context)
case template_path
when "base"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is quite readable, but you could also use when xxx then xxx to make a oneliner

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know but, on my screen, for some of them, I'm not able to see the beginning of the strings if I on-line these statements.

"<body>base</body>"

when "inherited"
"{% extends base %}"

when "page_with_title"
"<body><h1>{% block title %}Hello{% endblock %}</h1><p>Lorem ipsum</p></body>"

when "product"
"<body><h1>Our product: {{ name }}</h1>{% block info %}{% endblock %}</body>"

when "product_with_warranty"
"{% extends product %}{% block info %}<p>mandatory warranty</p>{% endblock %}"

when "product_with_static_price"
"{% extends product %}{% block info %}<h2>Some info</h2>{% block price %}<p>$42.00</p>{% endblock %}{% endblock %}"

else
template_path
end
end
end

class ExtendsTagTest < Minitest::Test
include Liquid

def setup
Liquid::Template.file_system = LayoutFileSystem.new
end

def test_template_extends_another_template
assert_template_result "<body>base</body>",
"{% extends base %}"
end

def test_template_extends_an_inherited_template
assert_template_result "<body>base</body>",
"{% extends inherited %}"
end

def test_template_can_pass_variables_to_the_parent_template
assert_template_result "<body><h1>Our product: Macbook</h1></body>",
"{% extends product %}", 'name' => 'Macbook'
end

def test_template_can_pass_variables_to_the_inherited_parent_template
assert_template_result "<body><h1>Our product: PC</h1><p>mandatory warranty</p></body>",
"{% extends product_with_warranty %}", 'name' => 'PC'
end

def test_template_does_not_render_statements_outside_blocks
assert_template_result "<body>base</body>",
"{% extends base %} Hello world"
end

def test_template_extends_another_template_with_a_single_block
assert_template_result "<body><h1>Hello</h1><p>Lorem ipsum</p></body>",
"{% extends page_with_title %}"
end

def test_template_overrides_a_block
assert_template_result "<body><h1>Sweet</h1><p>Lorem ipsum</p></body>",
"{% extends page_with_title %}{% block title %}Sweet{% endblock %}"
end

def test_template_has_access_to_the_content_of_the_overriden_block
assert_template_result "<body><h1>Hello world</h1><p>Lorem ipsum</p></body>",
"{% extends page_with_title %}{% block title %}{{ block.super }} world{% endblock %}"
end

def test_template_accepts_nested_blocks
assert_template_result "<body><h1>Our product: iPhone</h1><h2>Some info</h2><p>$42.00</p><p>(not on sale)</p></body>",
"{% extends product_with_static_price %}{% block info/price %}{{ block.super }}<p>(not on sale)</p>{% 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