diff --git a/Gemfile b/Gemfile index 29524192d..49720c001 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' -gemspec +gemspec name: 'locomotivecms-liquid' + gem 'stackprof', platforms: :mri_21 group :test do diff --git a/lib/liquid.rb b/lib/liquid.rb index 393454fc3..26835cefd 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -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 } diff --git a/lib/liquid/drops/inherited_block_drop.rb b/lib/liquid/drops/inherited_block_drop.rb new file mode 100644 index 000000000..c5ce39fc2 --- /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 diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb new file mode 100644 index 000000000..b8e37f00d --- /dev/null +++ b/lib/liquid/tags/extends.rb @@ -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 diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb new file mode 100644 index 000000000..9b2284f32 --- /dev/null +++ b/lib/liquid/tags/inherited_block.rb @@ -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 diff --git a/locomotivecms-liquid.gemspec b/locomotivecms-liquid.gemspec new file mode 100644 index 000000000..8b592094e --- /dev/null +++ b/locomotivecms-liquid.gemspec @@ -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 = ["tobi@leetsoft.com"] + 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 diff --git a/test/integration/tags/extends_tag_test.rb b/test/integration/tags/extends_tag_test.rb new file mode 100644 index 000000000..fdfc7a1d2 --- /dev/null +++ b/test/integration/tags/extends_tag_test.rb @@ -0,0 +1,104 @@ +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 < Minitest::Test + 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 + + # 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