forked from Shopify/liquid
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
template inheritance for liquid 4.0.0
- Loading branch information
Showing
7 changed files
with
322 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# encoding: utf-8 | ||
lib = File.expand_path('../lib/', __FILE__) | ||
$:.unshift lib unless $:.include?(lib) | ||
|
||
require "liquid/version" | ||
|
||
Gem::Specification.new do |s| | ||
s.name = "locomotivecms-liquid" | ||
s.version = Liquid::VERSION | ||
s.platform = Gem::Platform::RUBY | ||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." | ||
s.authors = ["Tobias Luetke", "Didier Lafforgue", "Jacques Crocker"] | ||
s.email = ["[email protected]"] | ||
s.homepage = "http://www.liquidmarkup.org" | ||
s.license = "MIT" | ||
#s.description = "A secure, non-evaling end user template engine with aesthetic markup." | ||
|
||
s.required_rubygems_version = ">= 1.3.7" | ||
|
||
s.test_files = Dir.glob("{test}/**/*") | ||
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md) | ||
|
||
s.extra_rdoc_files = ["History.md", "README.md"] | ||
|
||
s.require_path = "lib" | ||
|
||
s.add_development_dependency 'rake' | ||
s.add_development_dependency 'minitest' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
"<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 |