implement liquid inheritance (in progress)

This commit is contained in:
dinedine 2010-08-08 13:30:27 +02:00
parent cf3a177580
commit 4f06dab51e
12 changed files with 508 additions and 121 deletions

View File

@ -4,13 +4,16 @@ module Models
module Render
def render(context)
# ::Liquid::Template.parse(self.layout_template)
self.template.render(context)
if self.layout
self.layout.template.render(context)
else
::Liquid::Template.parse("{{ content_for_layout }}").render(context)
end
# self.template.render(context)
#
# if self.layout
# self.layout.template.render(context)
# else
# ::Liquid::Template.parse("{{ content_for_layout }}").render(context)
# end
end
end

View File

@ -5,11 +5,11 @@ class Layout < LiquidTemplate
embeds_many :parts, :class_name => 'PagePart'
## callbacks ##
before_save :build_parts_from_value
after_save :update_parts_in_pages
# before_save :build_parts_from_value
# after_save :update_parts_in_pages
## validations ##
validates_format_of :value, :with => Locomotive::Regexps::CONTENT_FOR_LAYOUT, :message => :missing_content_for_layout
# validates_format_of :value, :with => Locomotive::Regexps::CONTENT_FOR_LAYOUT, :message => :missing_content_for_layout
## methods ##

View File

@ -4,7 +4,7 @@ class Page
## Extensions ##
include Models::Extensions::Page::Tree
include Models::Extensions::Page::Parts
# include Models::Extensions::Page::Parts
include Models::Extensions::Page::Render
include Models::Extensions::Page::Templatized
@ -15,6 +15,8 @@ class Page
field :published, :type => Boolean, :default => false
field :cache_strategy, :default => 'none'
field :layout_template # FIXME: liquid inheritance
# allows newly pages to have a default body
attr_accessor :body
@ -40,7 +42,8 @@ class Page
scope :published, :where => { :published => true }
## behaviours ##
liquify_template :joined_parts
# liquify_template :joined_parts
liquify_template :layout_template
## methods ##

View File

@ -3,6 +3,10 @@ BOARD:
- refactor slugify method (use parameterize + create a module)
- [content types] the "display column" selector should not include file types
- add dom_id and css_class fields in page (body structure ?)
- liquid inheritance:
BACKLOG:
- notify accounts when new instance of models (opt): none, one or many accounts. Used for contact form.

View File

@ -5,84 +5,188 @@ Feature: Engine
Background:
Given I have the site: "test site" set up
Scenario: Simple Page
Given a simple page named "hello-world" with the body:
"""
Hello World
"""
When I view the rendered page at "/hello-world"
Then the rendered output should look like:
"""
Hello World
"""
# Scenario: Simple Page
# Given a page named "hello-world" with the template:
# """
# Hello World
# """
# When I view the rendered page at "/hello-world"
# Then the rendered output should look like:
# """
# Hello World
# """
#
# Scenario: Simple Page extending a layout with multiple blocks
# Given a layout named "layout_with_sidebar" with the source:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">{% block sidebar %}DEFAULT SIDEBAR CONTENT{% endblock %}</div>
# <div class="body">
# {% block body %}DEFAULT BODY CONTENT{% endblock %}
# </div>
# </div>
# <div class="footer"></div>
# """
# And a page named "hello-world-multiblocks" with the template:
# """
# {% extends 'layout_with_sidebar' %}
# {% block body %}Hello world{% endblock %}
# """
# When I view the rendered page at "/hello-world-multiblocks"
# Then the rendered output should look like:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">DEFAULT SIDEBAR CONTENT</div>
# <div class="body">
# Hello world
# </div>
# </div>
# <div class="footer"></div>
# """
Scenario: Simple Page with layout
Given a layout named "above_and_below" with the source:
"""
<div class="header"></div>
{{ content_for_layout }}
<div class="footer"></div>
"""
# Scenario: Simple Page extending a layout with multiple blocks which extends another template
# Given a layout named "layout_with_sidebar" with the source:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">{% block sidebar %}DEFAULT SIDEBAR CONTENT{% endblock %}</div>
# <div class="body">
# {% block body %}DEFAULT BODY CONTENT{% endblock %}
# </div>
# </div>
# <div class="footer"></div>
# """
# And a layout named "custom_layout_with_sidebar" with the source:
# """
# {% extends 'layout_with_sidebar' %}
# {% block sidebar %}Custom sidebar{% endblock %}
# {% block body %}Hello{% endblock %}
# """
# And a page named "hello-world-multiblocks" with the template:
# """
# {% extends 'custom_layout_with_sidebar' %}
# {% block body %}{{ block.super }} world{% endblock %}
# """
# When I view the rendered page at "/hello-world-multiblocks"
# Then the rendered output should look like:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">Custom sidebar</div>
# <div class="body">
# Hello world
# </div>
# </div>
# <div class="footer"></div>
# """
And a page named "hello-world-with-layout" with the layout "above_and_below" and the body:
"""
Hello World
"""
When I view the rendered page at "/hello-world-with-layout"
Then the rendered output should look like:
"""
<div class="header"></div>
Hello World
<div class="footer"></div>
"""
Scenario: Page with Parts
Scenario: Simple Page extending a layout with multiple embedded blocks which extends another template
Given a layout named "layout_with_sidebar" with the source:
"""
<div class="header"></div>
<div class="content">
<div class="sidebar">{{ content_for_sidebar }}</div>
<div class="sidebar">{% block sidebar %}DEFAULT SIDEBAR CONTENT{% endblock %}</div>
<div class="body">
{{ content_for_layout }}
{% block body %}Hello{% endblock %}
</div>
</div>
<div class="footer"></div>
"""
And a page named "hello-world-multipart" with the layout "layout_with_sidebar" and the body:
And a layout named "custom_layout_with_sidebar" with the source:
"""
IM IN UR BODY OUTPUTTING SUM CODEZ!!
{% extends 'layout_with_sidebar' %}
{% block body %}{{ block.super }} {% block main %}mister{% endblock %}{% endblock %}
"""
And the page named "hello-world-multipart" has the part "sidebar" with the content:
And a page named "hello-world-multiblocks" with the template:
"""
IM IN UR SIDEBAR PUTTING OUT LINKZ
{% extends 'custom_layout_with_sidebar' %}
{% block main %}{{ block.super }} Jacques{% endblock %}
"""
When I view the rendered page at "/hello-world-multipart"
When I view the rendered page at "/hello-world-multiblocks"
Then the rendered output should look like:
"""
<div class="header"></div>
<div class="content">
<div class="sidebar">IM IN UR SIDEBAR PUTTING OUT LINKZ</div>
<div class="sidebar">DEFAULT SIDEBAR CONTENT</div>
<div class="body">
IM IN UR BODY OUTPUTTING SUM CODEZ!!
Hello mister Jacques
</div>
</div>
<div class="footer"></div>
"""
@wip
Scenario: Simple Page with Admin Inline Editing
Given a simple page named "hello-world-inline" with the body:
"""
{% block hello %}Hello World{% endblock %}
"""
When And I'm an admin
And I view the rendered page at "/hello-world-inline"
Then the rendered output shoud look like:
"""
<div class="inline-editing" data-url="/admin/parts/XXXX" data-title="hello">Hello World</div>
"""
# Scenario: Simple Page with layout
# Given a layout named "above_and_below" with the source:
# """
# <div class="header"></div>
# {{ content_for_layout }}
# <div class="footer"></div>
# """
#
# And a page named "hello-world-with-layout" with the layout "above_and_below" and the body:
# """
# Hello World
# """
#
# When I view the rendered page at "/hello-world-with-layout"
# Then the rendered output should look like:
# """
# <div class="header"></div>
# Hello World
# <div class="footer"></div>
# """
#
# Scenario: Page with Parts
# Given a layout named "layout_with_sidebar" with the source:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">{{ content_for_sidebar }}</div>
# <div class="body">
# {{ content_for_layout }}
# </div>
# </div>
# <div class="footer"></div>
# """
# And a page named "hello-world-multipart" with the layout "layout_with_sidebar" and the body:
# """
# IM IN UR BODY OUTPUTTING SUM CODEZ!!
# """
#
# And the page named "hello-world-multipart" has the part "sidebar" with the content:
# """
# IM IN UR SIDEBAR PUTTING OUT LINKZ
# """
#
# When I view the rendered page at "/hello-world-multipart"
# Then the rendered output should look like:
# """
# <div class="header"></div>
# <div class="content">
# <div class="sidebar">IM IN UR SIDEBAR PUTTING OUT LINKZ</div>
# <div class="body">
# IM IN UR BODY OUTPUTTING SUM CODEZ!!
# </div>
# </div>
# <div class="footer"></div>
# """
#
# @wip
# Scenario: Simple Page with Admin Inline Editing
# Given a simple page named "hello-world-inline" with the body:
# """
# {% block hello %}Hello World{% endblock %}
# """
# When And I'm an admin
# And I view the rendered page at "/hello-world-inline"
# Then the rendered output shoud look like:
# """
# <div class="inline-editing" data-url="/admin/parts/XXXX" data-title="hello">Hello World</div>
# """
# {% block main %}Didier{% endblock %}
# {% block body %}{% block main %}{{ block.super }}Jacques{% endblock %}{% endblock %}
# {% block body %}Hello mister Jacques{% endblock %}

View File

@ -1,9 +1,9 @@
### Pages
# helps create a simple content page (parent: "index") with a slug, contents, and layout
def create_content_page(page_slug, page_contents, layout = nil)
def create_content_page(page_slug, page_contents, layout = nil, template = nil)
@home = @site.pages.where(:slug => "index").first || Factory(:page)
page = @site.pages.create(:slug => page_slug, :body => page_contents, :layout => layout, :parent => @home, :title => "some title", :published => true)
page = @site.pages.create(:slug => page_slug, :body => page_contents, :layout => layout, :parent => @home, :title => "some title", :published => true, :layout_template => template)
page.should be_valid
page
end
@ -21,6 +21,10 @@ Given /^a page named "([^"]*)" with the layout "([^"]*)" and the body:$/ do |pag
@page = create_content_page(page_slug, page_contents, layout)
end
Given /^a page named "([^"]*)" with the template:$/ do |page_slug, template|
@page = create_content_page(page_slug, '', nil, template)
end
# creates a layout
Given /^a layout named "([^"]*)" with the source:$/ do |layout_name, layout_body|
@layout = Factory(:layout, :name => layout_name, :value => layout_body, :site => @site)

View File

@ -0,0 +1,16 @@
module Locomotive
module Liquid
module Drops
class Block < ::Liquid::Drop
def initialize(block)
@block = block
end
def super
@block.call_super(@context)
end
end
end
end
end

View File

@ -0,0 +1,52 @@
module Locomotive
module Liquid
module Tags
class Block < ::Liquid::Block
Syntax = /(\w+)/
attr_accessor :parent
attr_reader :name
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@name = $1
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'block' - Valid syntax: block [name]")
end
# puts "** [Block/initialize] #{tag_name}, #{@name}, #{tokens.inspect}"
super if tokens
end
def render(context)
# puts "** [Block/render] #{@name} / #{@parent.inspect}"
context.stack do
context['block'] = Locomotive::Liquid::Drops::Block.new(self)
render_all(@nodelist, context)
end
end
def add_parent(nodelist)
if parent
parent.add_parent(nodelist)
else
self.parent = self.class.new(@tag_name, @name, nil)
parent.nodelist = nodelist
end
end
def call_super(context)
if parent
parent.render(context)
else
''
end
end
end
::Liquid::Template.register_tag('block', Block)
end
end
end

View File

@ -0,0 +1,117 @@
module Locomotive
module Liquid
module Tags
class Extends < ::Liquid::Block
Syntax = /(#{::Liquid::QuotedFragment})/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@template_name = $1
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'extends' - Valid syntax: extends [template]")
end
super
@blocks = @nodelist.inject({}) do |m, node|
m[node.name] = node if node.is_a?(Locomotive::Liquid::Tags::Block); m
end
# puts "** Extends #{@template_name} / #{@blocks.inspect} / #{@nodelist.inspect}"
end
def parse(tokens)
parse_all(tokens)
end
def render(context)
template = load_template(context)
parent_blocks = find_blocks(template.root)
# puts "** [Extends/render] parent_blocks = #{parent_blocks.inspect}"
@blocks.each do |name, block|
# puts "** [Extends/render] #{name}, #{block.inspect}"
if pb = parent_blocks[name]
pb.parent = block.parent
pb.add_parent(pb.nodelist)
pb.nodelist = block.nodelist
else
if is_extending?(template)
template.root.nodelist << block
end
end
end
template.render(context)
end
private
def parse_all(tokens)
# puts "** [parse_all] #{tokens.inspect}"
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
case token
when /^#{::Liquid::TagStart}/
if token =~ /^#{::Liquid::TagStart}\s*(\w+)\s*(.*)?#{::Liquid::TagEnd}$/
# fetch the tag from registered blocks
if tag = ::Liquid::Template.tags[$1]
# puts "** [parse_all] tag = #{$1}, #{$2}"
@nodelist << tag.new($1, $2, tokens)
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise ::Liquid::SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
end
when /^#{::Liquid::VariableStart}/
@nodelist << create_variable(token)
when ''
# pass
else
@nodelist << token
end
end
end
def load_template(context)
# puts "** load_template (#{context[@template_name]})"
layout = context.registers[:site].layouts.where(:slug => context[@template_name]).first
layout.template
end
def find_blocks(node, blocks={})
# puts "** find_blocks #{node.class.inspect} / #{blocks.keys.inspect}"
if node.respond_to?(:nodelist) && node.nodelist
# puts " ==> find_blocks nodelist = #{node.nodelist.inspect}"
node.nodelist.inject(blocks) do |b, node|
if node.is_a?(Locomotive::Liquid::Tags::Block)
b[node.name] = node
end
# else
find_blocks(node, b) # FIXME: add nested blocks
# end
b
end
end
blocks
end
def is_extending?(template)
template.root.nodelist.any? { |node| node.is_a?(Extends) }
end
end
::Liquid::Template.register_tag('extends', Extends)
end
end
end

63
perf/benchmark.rb Executable file
View File

@ -0,0 +1,63 @@
# require "rubygems"
# require "ruby-prof"
ENV["RAILS_ENV"] ||= 'test'
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# require File.dirname(__FILE__) + "/../config/application.rb"
# Mongoid.configure do |config|
# config.master = Mongo::Connection.new.db("locomotive_perf_test")
# end
%w{sites pages layouts}.each do |collection|
Mongoid.master.collection(collection).drop
end
puts "Starting benchmark..."
site = Site.create :name => 'Benchmark Website', :subdomain => 'benchmark'
layout_with_sidebar = site.layouts.create :name => 'with_sidebar', :value => %{
<html>
<head></head>
<body>
<div class="header"></div>
<div class="content">
<div class="sidebar">
{% block sidebar %}DEFAULT SIDEBAR CONTENT{% endblock %}
</div>
<div class="body">
{% block body %}DEFAULT BODY CONTENT{% endblock %}
</div>
</div>
<div class="footer"></div>
</body>
</html>
}
custom_layout_with_sidebar = site.layouts.create :name => 'custom_with_sidebar', :value => %{
\{% extends 'with_sidebar' %\}
\{% block sidebar %\}A sidebar here\{% endblock %\}
\{% block body %\}<div class="wrapper">\{% block main %\}DEFAULT MAIN CONTENT\{% endblock %\}</div>\{% endblock %\}
}
page = site.pages.create :title => 'Index', :slug => 'index', :layout_template => %{
\{% extends 'custom_with_sidebar' %\}
\{% block sidebar %\}\{\{ block.super \}\} / INDEX sidebar\{% endblock %\}
\{% block main %\}Lorem ipsum\{% endblock %\}
}
context = Liquid::Context.new({}, { 'site' => site }, { :site => site })
puts "====> \n#{page.render(context)}"
Benchmark.bm do |bm|
bm.report("Rendering page 10k times") do
10000.times do
Page.first.render(context)
end
end
end
# without liquify (macbook white): User System Total Real
# Rendering page 10k times 22.650000 6.220000 28.870000 ( 30.294338)

View File

@ -69,6 +69,28 @@ end
## Layouts ##
Factory.define :layout do |l|
l.name '1 main column + sidebar'
l.value %{<html>
<head>
<title>My website</title>
</head>
<body>
<div id="sidebar">
\{% block sidebar %\}
DEFAULT SIDEBAR CONTENT
\{% endblock %\}
</div>
<div id="main">
\{% block main %\}
DEFAULT MAIN CONTENT
\{% endblock %\}
</div>
</body>
</html>}
l.site { Site.where(:subdomain => "acme").first || Factory(:site) }
end
Factory.define :base_layout, :parent => :layout do |l|
l.name '1 main column + sidebar'
l.value %{<html>
<head>
@ -79,7 +101,6 @@ Factory.define :layout do |l|
<div id="main">\{\{ content_for_layout | textile \}\}</div>
</body>
</html>}
l.site { Site.where(:subdomain => "acme").first || Factory(:site) }
end

View File

@ -6,59 +6,59 @@ describe Layout do
Factory.build(:layout).should be_valid
end
## validations ##
it 'should validate presence of content_for_layout in value' do
layout = Factory.build(:layout, :value => 'without content_for_layout')
layout.should_not be_valid
layout.errors[:value].should == ["should contain 'content_for_layout' liquid tag"]
end
context 'dealing with page parts' do
before(:each) do
@layout = Factory.build(:layout)
end
it 'should have 2 parts' do
@layout.send(:build_parts_from_value)
@layout.parts.size.should == 2
@layout.parts.first.name.should == 'Body'
@layout.parts.first.slug.should == 'layout'
@layout.parts.last.name.should == 'Left sidebar'
@layout.parts.last.slug.should == 'left_sidebar'
end
it 'should not add parts to pages if layout does not change' do
@layout.stubs(:value_changed?).returns(false)
page = Factory.build(:page, :layout => @layout, :site => nil)
page.expects(:update_parts!).never
@layout.pages << page
@layout.save
end
it 'should add parts to pages if layout changes' do
@layout.value = @layout.value + "..."
page = Factory.build(:page, :layout => @layout, :site => nil)
page.expects(:update_parts!)
@layout.pages << page
@layout.save
end
end
context 'parsing liquid template' do
before(:each) do
@layout = Factory.build(:layout)
end
it 'should not raise an error if template is empty' do
@layout.template.should be_nil
end
end
# ## validations ##
#
# it 'should validate presence of content_for_layout in value' do
# layout = Factory.build(:layout, :value => 'without content_for_layout')
# layout.should_not be_valid
# layout.errors[:value].should == ["should contain 'content_for_layout' liquid tag"]
# end
#
# context 'dealing with page parts' do
#
# before(:each) do
# @layout = Factory.build(:layout)
# end
#
# it 'should have 2 parts' do
# @layout.send(:build_parts_from_value)
# @layout.parts.size.should == 2
#
# @layout.parts.first.name.should == 'Body'
# @layout.parts.first.slug.should == 'layout'
#
# @layout.parts.last.name.should == 'Left sidebar'
# @layout.parts.last.slug.should == 'left_sidebar'
# end
#
# it 'should not add parts to pages if layout does not change' do
# @layout.stubs(:value_changed?).returns(false)
# page = Factory.build(:page, :layout => @layout, :site => nil)
# page.expects(:update_parts!).never
# @layout.pages << page
# @layout.save
# end
#
# it 'should add parts to pages if layout changes' do
# @layout.value = @layout.value + "..."
# page = Factory.build(:page, :layout => @layout, :site => nil)
# page.expects(:update_parts!)
# @layout.pages << page
# @layout.save
# end
#
# end
#
# context 'parsing liquid template' do
#
# before(:each) do
# @layout = Factory.build(:layout)
# end
#
# it 'should not raise an error if template is empty' do
# @layout.template.should be_nil
# end
#
# end
end