-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
base: main
Are you sure you want to change the base?
Changes from all commits
3574889
3f0f76a
a2c3766
7d14d7e
a6e6e70
7e4eacd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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 | ||
|
||
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 |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is quite readable, but you could also use There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.