Skip to content

Commit

Permalink
template inheritance for liquid 4.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
did committed Jan 25, 2015
1 parent 3476a55 commit 31a7160
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 2 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
source 'https://rubygems.org'

gemspec
gemspec name: 'locomotivecms-liquid'

gem 'stackprof', platforms: :mri_21

group :test do
Expand Down
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

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
29 changes: 29 additions & 0 deletions locomotivecms-liquid.gemspec
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
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"
"<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

0 comments on commit 31a7160

Please sign in to comment.