rendering engine + liquid tags/drops/filters + rspec tests + fix small bugs

This commit is contained in:
dinedine 2010-05-31 01:57:33 +02:00
parent 311903a43d
commit 9447386f0e
46 changed files with 1266 additions and 121 deletions

10
Gemfile
View File

@ -20,11 +20,11 @@ gem "rmagick"
# Development environment
group :development do
# Using mongrel instead of webrick (default server)
# gem "mongrel"
# gem "cgi_multipart_eof_fix"
# gem "fastthread"
# gem "mongrel_experimental"
# Using mongrel instead of webrick (default server)
gem "mongrel"
gem "cgi_multipart_eof_fix"
gem "fastthread"
gem "mongrel_experimental"
end
group :test do

View File

@ -2,11 +2,12 @@ class PagesController < ActionController::Base
include Locomotive::Routing::SiteDispatcher
include Locomotive::Render
before_filter :require_site
def show
logger.debug "fullpath = #{request.fullpath}"
# @page = current_site.pages.find
render_locomotive_page
end
end

View File

@ -17,6 +17,8 @@ class Asset
## validations ##
validates_presence_of :name, :source
## behaviours ##
## methods ##
%w{image stylesheet javascript pdf video audio}.each do |type|
@ -24,5 +26,9 @@ class Asset
self.content_type == type
end
end
def to_liquid
{ :url => self.source.url }.merge(self.attributes)
end
end

View File

@ -13,6 +13,7 @@ class AssetCollection
## behaviours ##
custom_fields_for :assets
liquid_methods :name, :ordered_assets
## callbacks ##
before_validate :normalize_slug

View File

@ -28,9 +28,14 @@ class ContentType
## methods ##
def ordered_contents
def ordered_contents(conditions = {})
column = self.order_by.to_sym
self.contents.sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 0) }
(if conditions.nil? || conditions.empty?
self.contents
else
self.contents.where(conditions)
end).sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 0) }
end
def sort_contents!(order)

View File

@ -12,11 +12,13 @@ class Layout < LiquidTemplate
validates_format_of :value, :with => Locomotive::Regexps::CONTENT_FOR_LAYOUT, :message => :missing_content_for_layout
## methods ##
protected
def build_parts_from_value
if self.value_changed? || self.new_record?
self.parts.each { |p| p.disabled = true }
self.value.scan(Locomotive::Regexps::CONTENT_FOR).each do |attributes|
slug = attributes[0].strip.downcase
name = attributes[1].strip.gsub("\"", '')
@ -25,6 +27,7 @@ class Layout < LiquidTemplate
if part = self.parts.detect { |p| p.slug == slug }
part.name = name if name.present?
part.disabled = false
else
self.parts.build :slug => slug, :name => name || slug
end

View File

@ -6,16 +6,24 @@ class LiquidTemplate
field :name
field :slug
field :value
field :template, :type => Binary
## associations ##
belongs_to_related :site
## callbacks ##
before_validate :normalize_slug
before_validate :store_template
## validations ##
validates_presence_of :site, :name, :slug, :value
validates_uniqueness_of :slug, :scope => :site_id #[:site_id, :_type]
validates_uniqueness_of :slug, :scope => :site_id
## methods ##
def template
Marshal.load(read_attribute(:template).to_s) rescue nil
end
protected
@ -23,5 +31,14 @@ class LiquidTemplate
self.slug = self.name.clone if self.slug.blank? && self.name.present?
self.slug.slugify!(:without_extension => true, :downcase => true) if self.slug.present?
end
def store_template
begin
parsed_template = Liquid::Template.parse(self.value)
self.template = BSON::Binary.new(Marshal.dump(parsed_template))
rescue Liquid::SyntaxError => error
self.errors.add :value, :liquid_syntax_error
end
end
end

View File

@ -6,10 +6,12 @@ class Page
## fields ##
field :title
field :slug
field :fullpath
field :published, :type => Boolean, :default => false
field :keywords
field :description
field :position, :type => Integer
field :template, :type => Binary
## associations ##
belongs_to_related :site
@ -19,6 +21,8 @@ class Page
## callbacks ##
before_validate :reset_parent
before_validate :normalize_slug
before_validate :store_template
before_save { |p| p.fullpath = p.fullpath(true) }
before_save { |p| p.parent_id = nil if p.parent_id.blank? }
before_save :change_parent
before_create { |p| p.parts << PagePart.build_body_part if p.parts.empty? }
@ -34,6 +38,8 @@ class Page
## named scopes ##
named_scope :latest_updated, :order_by => [[:updated_at, :desc]], :limit => Locomotive.config.lastest_items_nb
named_scope :index, :where => { :slug => 'index', :depth => 0 }
named_scope :not_found, :where => { :slug => '404', :depth => 0 }
## behaviours ##
acts_as_tree :order => ['position', 'asc']
@ -65,16 +71,27 @@ class Page
child.save
end
end
def route
return self.slug if self.index? || self.not_found?
slugs = self.self_and_ancestors.map(&:slug)
slugs.shift
File.join slugs
def fullpath(force = false)
if read_attribute(:fullpath).present? && !force
return read_attribute(:fullpath)
end
if self.index? || self.not_found?
self.slug
else
slugs = self.self_and_ancestors.map(&:slug)
slugs.shift
File.join slugs
end
end
def url
"http://#{self.site.domains.first}/#{self.route}.html"
"http://#{self.site.domains.first}/#{self.fullpath}.html"
end
def template
Marshal.load(read_attribute(:template).to_s) rescue nil
end
def ancestors
@ -82,6 +99,16 @@ class Page
self.class.find(self.path.clone << nil) # bug in mongoid (it does not handle array with one element)
end
def render(context)
self.template.render(context)
if self.layout
self.layout.template.render(context)
else
Liquid::Template.parse("{{ content_for_layout }}").render(context)
end
end
protected
def do_not_remove_index_and_404_pages
@ -163,4 +190,14 @@ class Page
self.slug = self.title.clone if self.slug.blank? && self.title.present?
self.slug.slugify!(:without_extension => true) if self.slug.present?
end
def store_template
begin
parsed_template = Liquid::Template.parse(self.parts.enabled.map(&:template).join(''))
self.template = BSON::Binary.new(Marshal.dump(parsed_template))
rescue Liquid::SyntaxError => error
self.errors.add :template, :liquid_syntax_error
end
end
end

View File

@ -22,6 +22,10 @@ class PagePart
## methods ##
def template
"{% capture content_for_#{self.slug} %}#{self.value}{% endcapture %}"
end
def self.build_body_part
self.new({
:name => I18n.t('attributes.defaults.page_parts.name'),
@ -29,4 +33,5 @@ class PagePart
:slug => 'layout'
})
end
end

View File

@ -33,6 +33,7 @@ class Site
named_scope :match_domain_with_exclusion_of, lambda { |domain, site| { :where => { :domains => domain, :_id.ne => site.id } } }
## behaviours ##
liquid_methods :name
## methods ##
@ -56,7 +57,7 @@ class Site
def domains_with_subdomain
((self.domains || []) + ["#{self.subdomain}.#{Locomotive.config.default_domain}"]).uniq
end
protected
def domains_must_be_valid_and_unique

View File

@ -3,8 +3,8 @@ class ThemeAsset
include Mongoid::Timestamps
## fields ##
field :slug, :type => String
field :content_type, :type => String
field :slug
field :content_type
field :width, :type => Integer
field :height, :type => Integer
field :size, :type => Integer
@ -22,6 +22,7 @@ class ThemeAsset
validate :extname_can_not_be_changed
validates_presence_of :site, :source
validates_presence_of :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? }
validates_uniqueness_of :slug, :scope => [:site_id, :content_type]
validates_integrity_of :source
## accessors ##
@ -68,6 +69,10 @@ class ThemeAsset
"#{self.slug}#{File.extname(self.source.file.original_filename)}"
end
end
def to_liquid
{ :url => self.source.url }.merge(self.attributes)
end
protected

View File

@ -6,7 +6,7 @@
- content_for :buttons do
= admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'add'
%p= t('.help')
%p= t('.help', :url => @asset.source.url)
= semantic_form_for @asset, :url => admin_theme_asset_url(@asset), :html => { :multipart => true } do |form|

View File

@ -84,6 +84,8 @@ en:
new: new snippet
new:
title: New snippet
edit:
title: Editing snippet
sites:
new:
@ -118,7 +120,8 @@ en:
new:
title: New file
edit:
title: "Editing {{file}}"
title: "Editing {{file}}"
help: "You can use it by copying/pasting the following url: {{url}}"
form:
choose_file: Choose file
choose_plain_text: Choose plain text

View File

@ -8,6 +8,7 @@ en:
protected_page: "You can not remove index or 404 pages"
extname_changed: "New file does not have the original extension"
array_too_short: "is too small (minimum element number is {{count}})"
liquid_syntax_error: "Syntax error in page parts, please check the syntax"
attributes:
defaults:
@ -22,3 +23,7 @@ en:
body: "Content goes here"
page_parts:
name: "Body"
pagination:
previous: "&laquo; Previous"
next: "Next &raquo;"

View File

@ -1,20 +1,24 @@
BOARD:
- liquid rendering engine
- theme assets
- theme assets picker (???)
BACKLOG:
- theme assets
- asset collections: custom resizing if image
- file type (icons)
- devise messages in French
- localize devise emails
- refactoring admin crud (pages + layouts + snippets)
- refactoring page.rb => create module pagetree
- refactoring: CustomFields::CustomField => CustomFields::Field
- optimization custom_fields: use dynamic class for a collection instead of modifying the metaclass each time we build an item
BUGS:
- theme assets: disable version if not image
- assets uploader: remove old files if new one
NICE TO HAVE:
- asset collections: custom resizing if image
DONE:
x admin layout
@ -78,4 +82,7 @@ x content types / models (CRUD)
x pre-select the first custom field as the highlighted one
x contents (CRUD)
x sort contents
x contents sub menu => BUG
x contents sub menu => BUG
x liquid rendering engine
x contents pagination
x how to disable a page part in layout ? (BUG)

View File

@ -1,5 +1,6 @@
# require 'locomotive/patches'
require 'locomotive/configuration'
require 'locomotive/liquid'
module Locomotive

5
lib/locomotive/liquid.rb Normal file
View File

@ -0,0 +1,5 @@
%w{. tags drops filters}.each do |dir|
Dir[File.join(File.dirname(__FILE__), 'liquid', dir, '*.rb')].each { |lib| require lib }
end
::Liquid::Template.file_system = Locomotive::Liquid::DbFileSystem.new # enable snippets

View File

@ -0,0 +1,22 @@
module Locomotive
module Liquid
# Works only with snippets
class DbFileSystem
def read_template_file(site, template_path)
raise FileSystemError, "Illegal snippet name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
snippet = site.snippets.where(:slug => template_path).first
raise FileSystemError, "No such snippet '#{template_path}'" if snippet.nil?
snippet.template
end
end
end
end

View File

@ -0,0 +1,23 @@
module Locomotive
module Liquid
module Drops
class AssetCollections < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
@site.asset_collections.where(:slug => meth.to_s)
end
end
end
end
end

View File

@ -0,0 +1,55 @@
# Code taken from Mephisto sources (http://mephistoblog.com/)
module Locomotive
module Liquid
module Drops
class Base < ::Liquid::Drop
class_inheritable_reader :liquid_attributes
write_inheritable_attribute :liquid_attributes, []
attr_reader :source
delegate :hash, :to => :source
def initialize(source)
unless source.nil?
@source = source
@liquid = liquid_attributes.flatten.inject({}) { |h, k| h.update k.to_s => @source.send(k) }
end
end
def id
(@source.respond_to?(:id) ? @source.id : nil) || 'new'
end
def before_method(method)
@liquid[method.to_s]
end
# converts an array of records to an array of liquid drops
def self.liquify(*records, &block)
i = -1
records =
records.inject [] do |all, r|
i+=1
attrs = (block && block.arity == 1) ? [r] : [r, i]
all << (block ? block.call(*attrs) : r.to_liquid)
all
end
records.compact!
records
end
protected
def liquify(*records, &block)
self.class.liquify(*records, &block)
end
end
end
end
end

View File

@ -0,0 +1,25 @@
module Locomotive
module Liquid
module Drops
class Content < Base
@@forbidden_attributes = %w{_id _version _index}
def before_method(meth)
return '' if @source.nil?
if not @@forbidden_attributes.include?(meth.to_s)
@source.send(meth)
end
end
end
end
end
end

View File

@ -0,0 +1,70 @@
module Locomotive
module Liquid
module Drops
class Contents < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
type = @site.content_types.where(:slug => meth.to_s).first
ProxyCollection.new(@site, type)
end
end
class ProxyCollection < ::Liquid::Drop
def initialize(site, content_type)
@site = site
@content_type = content_type
@collection = nil
end
def first
content = @content_type.ordered_contents(@context['with_scope']).first
build_content_drop(content) unless content.nil?
end
def last
content = @content_type.ordered_contents(@context['with_scope']).last
build_content_drop(content) unless content.nil?
end
def each(&block)
@collection ||= @content_type.ordered_contents(@context['with_scope'])
to_content_drops.each(&block)
end
def to_content_drops
@collection.map { |c| build_content_drop(c) }
end
def build_content_drop(content)
Locomotive::Liquid::Drops::Content.new(content)
end
def paginate(options = {})
@collection ||= @content_type.ordered_contents(@context['with_scope']).paginate(options)
{
:collection => to_content_drops,
:current_page => @collection.current_page,
:previous_page => @collection.previous_page,
:next_page => @collection.next_page,
:total_entries => @collection.total_entries,
:total_pages => @collection.total_pages,
:per_page => @collection.per_page
}
end
end
end
end
end

View File

@ -0,0 +1,24 @@
module Locomotive
module Liquid
module Drops
class Javascripts < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = site.theme_assets.where(:content_type => 'javascript', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -0,0 +1,24 @@
module Locomotive
module Liquid
module Drops
class Stylesheets < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = @site.theme_assets.where(:content_type => 'stylesheet', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -0,0 +1,36 @@
module Locomotive
module Liquid
module Filters
module Date
def localized_date(input, *args)
format, locale = args[0], args[1] rescue 'en'
date = input.is_a?(String) ? Time.parse(input) : input
if format.to_s.empty?
return input.to_s
end
date = input.is_a?(String) ? Time.parse(input) : input
if date.respond_to?(:strftime)
I18n.locale = locale
I18n.l date, :format => format
else
input
end
end
end
::Liquid::Template.register_filter(Date)
end
end
end

View File

@ -0,0 +1,118 @@
module Locomotive
module Liquid
module Filters
module Html
# Write the link to a stylesheet resource
# input: url of the css file
def stylesheet_tag(input)
return '' if input.nil?
input = "#{input}.css" unless input.ends_with?('.css')
%{<link href="#{input}" media="screen" rel="stylesheet" type="text/css" />}
end
# Write the link to javascript resource
# input: url of the javascript file
def javascript_tag(input)
return '' if input.nil?
input = "#{input}.js" unless input.ends_with?('.js')
%{<script src="#{input}" type="text/javascript"></script>}
end
# Write an image tag
# input: url of the image OR asset drop
def image_tag(input, *args)
image_options = inline_options(args_to_options(args))
"<img src=\"#{File.join('/', get_path_from_asset(input))}\" #{image_options}/>"
end
# Embed a flash movie into a page
# input: url of the flash movie OR asset drop
# width: width (in pixel or in %) of the embedded movie
# height: height (in pixel or in %) of the embedded movie
def flash_tag(input, *args)
path = get_path_from_asset(input)
embed_options = inline_options(args_to_options(args))
%{
<object #{embed_options}>
<param name="movie" value="#{path}" />
<embed src="#{path}" #{embed_options}/>
</embed>
</object>
}.gsub(/ >/, '>').strip
end
# Render the navigation for a paginated collection
def default_pagination(paginate, *args)
return '' if paginate['parts'].empty?
options = args_to_options(args)
previous_link = (if paginate['previous'].blank?
"<span class=\"disabled prev_page\">#{I18n.t('pagination.previous')}</span>"
else
"<a href=\"#{paginate['previous']['url']}\" class=\"prev_page\">#{I18n.t('pagination.previous')}</a>"
end)
links = ""
paginate['parts'].each do |part|
links << (if part['is_link']
"<a href=\"#{part['url']}\">#{part['title']}</a>"
elsif part['hellip_break']
"<span class=\"gap\">#{part['title']}</span>"
else
"<span class=\"current\">#{part['title']}</span>"
end)
end
next_link = (if paginate['next'].blank?
"<span class=\"disabled next_page\">#{I18n.t('pagination.next')}</span>"
else
"<a href=\"#{paginate['next']['url']}\" class=\"next_page\">#{I18n.t('pagination.next')}</a>"
end)
%{<div class="pagination #{options[:css]}">
#{previous_link}
#{links}
#{next_link}
</div>}
end
protected
# Convert an array of properties ('key:value') into a hash
# Ex: ['width:50', 'height:100'] => { :width => '50', :height => '100' }
def args_to_options(*args)
options = {}
args.flatten.each do |a|
if (a =~ /^(.*):(.*)$/)
options[$1.to_sym] = $2
end
end
options
end
# Write options (Hash) into a string according to the following pattern:
# <key1>="<value1>", <key2>="<value2", ...etc
def inline_options(options = {})
return '' if options.empty?
(options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ') << ' '
end
# Get the path to be used in html tags such as image_tag, flash_tag, ...etc
# input: url (String) OR asset drop
def get_path_from_asset(input)
input.respond_to?(:url) ? input.url : input
end
end
::Liquid::Template.register_filter(Html)
end
end
end

View File

@ -0,0 +1,35 @@
module Locomotive
module Liquid
module Filters
module Misc
def underscore(input)
input.to_s.gsub(' ', '_').gsub('/', '_').underscore
end
def dasherize(input)
input.to_s.gsub(' ', '-').gsub('/', '-').dasherize
end
def concat(input, *args)
result = input.to_s
args.flatten.each { |a| result << a.to_s }
result
end
def modulo(word, index, modulo)
(index.to_i + 1) % modulo == 0 ? word : ''
end
end
::Liquid::Template.register_filter(Misc)
end
end
end

View File

@ -0,0 +1,27 @@
module Locomotive
module Liquid
module Tags
class Blueprint < ::Liquid::Tag
def render(context)
%{
<link href="/stylesheets/blueprint/screen.css" media="screen, projection" rel="stylesheet" type="text/css" />
<link href="/stylesheets/blueprint/print.css" media="print" rel="stylesheet" type="text/css" />
<!--[if IE]>
<link href="/stylesheets/blueprint/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
<![endif]-->
}
end
end
::Liquid::Template.register_tag('blueprint_stylesheets', Blueprint)
end
end
end

View File

@ -0,0 +1,23 @@
module Liquid
module Locomotive
module Tags
class Jquery < ::Liquid::Tag
def render(context)
%{
<script src="/javascripts/jquery.js" type="text/javascript"></script>
<script src="/javascripts/jquery.ui.js" type="text/javascript"></script>
}
end
end
::Liquid::Template.register_tag('jQuery', Jquery)
end
end
end

View File

@ -0,0 +1,100 @@
module Locomotive
module Liquid
module Tags
# Paginate a collection
#
# Usage:
#
# {% paginate contents.projects by 5 %}
# {% for project in paginate.collection %}
# {{ project.name }}
# {% endfor %}
# {% endpaginate %}
#
class Paginate < ::Liquid::Block
Syntax = /(#{::Liquid::Expression}+)\s+by\s+([0-9]+)/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@collection_name = $1
@per_page = $2.to_i
else
raise SyntaxError.new("Syntax Error in 'paginate' - Valid syntax: paginate [collection] by [number]")
end
super
end
def render(context)
context.stack do
collection = context[@collection_name]
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection.nil?
pagination = collection.paginate({
:page => context['current_page'],
:per_page => @per_page }).stringify_keys!
page_count, current_page = pagination['total_pages'], pagination['current_page']
path = context['page'].path rescue '/'
pagination['previous'] = link(I18n.t('pagination.previous'), current_page - 1, path) if pagination['previous_page']
pagination['next'] = link(I18n.t('pagination.next'), current_page + 1, path) if pagination['next_page']
pagination['parts'] = []
hellip_break = false
if page_count > 1
1.upto(page_count) do |page|
if current_page == page
pagination['parts'] << no_link(page)
elsif page == 1
pagination['parts'] << link(page, page, path)
elsif page == page_count - 1
pagination['parts'] << link(page, page, path)
elsif page <= current_page - window_size or page >= current_page + window_size
next if hellip_break
pagination['parts'] << no_link('&hellip;')
hellip_break = true
next
else
pagination['parts'] << link(page, page, path)
end
hellip_break = false
end
end
context['paginate'] = pagination
render_all(@nodelist, context)
end
end
private
def window_size
3
end
def no_link(title)
{ 'title' => title, 'is_link' => false, 'hellip_break' => title == '&hellip;' }
end
def link(title, page, path)
{ 'title' => title, 'url' => path + "?page=#{page}", 'is_link' => true}
end
end
::Liquid::Template.register_tag('paginate', Paginate)
end
end
end

View File

@ -0,0 +1,42 @@
module Locomotive
module Liquid
module Tags
class Snippet < ::Liquid::Include
def render(context)
site = context.registers[:site]
partial = ::Liquid::Template.file_system.read_template_file(site, context[@template_name])
variable = context[@variable_name || @template_name[1..-2]]
context.stack do
@attributes.each do |key, value|
context[key] = context[value]
end
output = (if variable.is_a?(Array)
variable.collect do |variable|
context[@template_name[1..-2]] = variable
partial.render(context)
end
else
context[@template_name[1..-2]] = variable
partial.render(context)
end)
output
end
end
end
::Liquid::Template.register_tag('include', Snippet)
end
end
end

View File

@ -0,0 +1,45 @@
module Locomotive
module Liquid
module Tags
class WithScope < ::Liquid::Block
def initialize(tag_name, markup, tokens)
@attributes = {}
markup.scan(::Liquid::TagAttributes) do |key, value|
@attributes[key] = value
end
super
end
def render(context)
context.stack do
context['with_scope'] = decode(@attributes)
render_all(@nodelist, context)
end
end
private
def decode(attributes)
attributes.each_pair do |key, value|
attributes[key] = (case value
when /true|false/ then value == 'true'
when /[0-9]+/ then value.to_i
when /'(\S+)'/ then $1
else
value
end)
end
end
end
::Liquid::Template.register_tag('with_scope', WithScope)
end
end
end

54
lib/locomotive/render.rb Normal file
View File

@ -0,0 +1,54 @@
module Locomotive
module Render
extend ActiveSupport::Concern
module InstanceMethods
protected
def render_locomotive_page
page = locomotive_page
redirect_to application_root_url and return if page.nil?
output = page.render(locomotive_context)
prepare_and_set_response(output)
end
def locomotive_page
path = request.fullpath.clone
path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '')
path.gsub!(/^\//, '')
path = 'index' if path.blank?
current_site.pages.where(:fullpath => path).first ||
current_site.pages.not_found.first
end
def locomotive_context
assigns = {
'site' => current_site,
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
# 'theme_assets' => Locomotive::Liquid::Drops::ThemeAssets.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
'contents' => Locomotive::Liquid::Drops::Contents.new(current_site),
'current_page' => self.params[:page]
}
registers = { :controller => self, :site => current_site }
::Liquid::Context.new(assigns, registers)
end
def prepare_and_set_response(output)
response.headers["Content-Type"] = 'text/html; charset=utf-8'
render :text => output, :layout => false, :status => :ok
end
end
end
end

View File

@ -1,82 +0,0 @@
module Locomotive
class Sitemap
attr_accessor :index, :children, :not_found
def initialize(site)
self.children = site.pages.roots.to_a
self.index = self.children.detect { |p| p.index? }
self.not_found = self.children.detect { |p| p.not_found? }
self.children.delete(self.index)
self.children.delete(self.not_found)
end
def empty?
self.index.nil? && self.not_found.nil? && self.children.empty?
end
def self.build(site); self.new(site); end
end
# module Sitemap
#
# def self.build(pages)
# return [] if pages.empty?
#
# # pages = pages.to_a.clone # make a secure copy
# dictionary = build_dictionary(pages)
#
# returning [] do |map|
# map << (index = pages.detect { |p| p.index? })
# pages.delete(index)
#
# not_found = pages.detect { |p| p.not_found? }
# pages.delete(not_found)
#
# add_children()
#
# map << not_found
# end
# end
#
# protected
#
# def self.add_children(map, children, dictionary)
#
# end
#
# def self.build_dictionary(pages)
# returning({}) do |hash|
# hash[pages.id] = pages
# end
# end
#
# # def self.build(pages)
# # return Folder.new if pages.empty?
# #
# # pages = pages.to_a.clone # make a secure copy
# #
# # root = Folder.new :root => pages.delete_if { |p| p.path == 'index' }
# #
# # pages.each do |page|
# #
# # end
# # end
# #
# # class Folder
# # attr_accessor :root, :depth, :children
# #
# # def initialize(attributes = {})
# # self.root = attributes[:root]
# # self.depth = self.root.depth
# # end
# #
# # end
#
# end
end

View File

@ -17,16 +17,16 @@ $(document).ready(function() {
$('#parts code textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
var refreshParts = function(parts) {
console.log('refreshParts');
// console.log('refreshParts');
$('#page-parts .nav a').removeClass('enabled');
$(parts).each(function() {
console.log("iterating..." + this.slug);
// console.log("iterating..." + this.slug);
var control = $('#control-part-' + this.slug);
// adding missing part
if (control.size() == 0) {
console.log('adding part');
// console.log('adding part');
var nbParts = $('#page-parts .nav a').size();
$('#page-parts .nav .clear').before('<a id="control-part-' + this.slug + '" class="enabled part-' + nbParts + '" href="#parts-' + (nbParts + 1) + '"><span>' + this.name + '</span></a>');
@ -75,7 +75,7 @@ $(document).ready(function() {
return ;
var url = $('#page_layout_id').attr('data_url').replace('_id_to_replace_', $('#page_layout_id').val());
$.get(url, '', function(data) { refreshParts(data.parts) }, 'json');
$.get(url, '', function(data) { refreshParts(data.parts); }, 'json');
}
$('#page_layout_id').change(loadPartsFromLayout);

View File

@ -55,7 +55,7 @@ Factory.define :snippet do |s|
s.association :site, :factory => :site
s.name 'My website title'
s.slug 'header'
s.value %{<title>Acme</title}
s.value %{<title>Acme</title>}
end
## Theme assets ##

View File

@ -0,0 +1,82 @@
require 'spec_helper'
describe Locomotive::Liquid::Filters::Html do
include Locomotive::Liquid::Filters::Html
it 'should return a link tag for a stylesheet file' do
result = "<link href=\"main.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />"
stylesheet_tag('main.css').should == result
stylesheet_tag('main').should == result
stylesheet_tag(nil).should == ''
end
it 'should return a script tag for a javascript file' do
result = %{<script src="main.js" type="text/javascript"></script>}
javascript_tag('main.js').should == result
javascript_tag('main').should == result
javascript_tag(nil).should == ''
end
it 'should return an image tag without paramaters' do
image_tag('foo.jpg').should == "<img src=\"/foo.jpg\" />"
end
it 'should return an image tag with size' do
image_tag('foo.jpg', 'width:100', 'height:50').should == "<img src=\"/foo.jpg\" height=\"50\" width=\"100\" />"
end
it 'should return a flash tag without parameters' do
flash_tag('foo.flv').should == %{
<object>
<param name="movie" value="foo.flv" />
<embed src="foo.flv" />
</embed>
</object>
}.strip
end
it 'should return a flash tag with size' do
flash_tag('foo.flv', 'width:100', 'height:50').should == %{
<object height=\"50\" width=\"100\">
<param name="movie" value="foo.flv" />
<embed src="foo.flv" height=\"50\" width=\"100\" />
</embed>
</object>
}.strip
end
it 'should return a navigation block for the pagination' do
pagination = {
"previous" => nil,
"parts" => [
{ 'title' => '1', 'is_link' => false },
{ 'title' => '2', 'is_link' => true, 'url' => '/?page=2' },
{ 'title' => '&hellip;', 'is_link' => false, 'hellip_break' => true },
{ 'title' => '5', 'is_link' => true, 'url' => '/?page=5' }
],
"next" => { 'title' => 'next', 'is_link' => true, 'url' => '/?page=2' }
}
html = default_pagination(pagination, 'css:flickr_pagination')
html.should match(/<div class="pagination flickr_pagination">/)
html.should match(/<span class="disabled prev_page">&laquo; Previous<\/span>/)
html.should match(/<a href="\/\?page=2">2<\/a>/)
html.should match(/<span class=\"gap\">\&hellip;<\/span>/)
html.should match(/<a href="\/\?page=2" class="next_page">Next &raquo;<\/a>/)
pagination.merge!({
'previous' => { 'title' => 'previous', 'is_link' => true, 'url' => '/?page=4' },
'next' => nil
})
html = default_pagination(pagination, 'css:flickr_pagination')
html.should_not match(/<span class="disabled prev_page">&laquo; Previous<\/span>/)
html.should match(/<a href="\/\?page=4" class="prev_page">&laquo; Previous<\/a>/)
html.should match(/<span class="disabled next_page">Next &raquo;<\/span>/)
pagination.merge!({ 'parts' => [] })
html = default_pagination(pagination, 'css:flickr_pagination')
html.should == ''
end
end

View File

@ -0,0 +1,36 @@
require 'spec_helper'
describe Locomotive::Liquid::Filters::Misc do
include Locomotive::Liquid::Filters::Misc
it 'should underscore an input' do
underscore('foo').should == 'foo'
underscore('home page').should == 'home_page'
underscore('My foo Bar').should == 'my_foo_bar'
underscore('foo/bar').should == 'foo_bar'
underscore('foo/bar/index').should == 'foo_bar_index'
end
it 'should dasherize an input' do
dasherize('foo').should == 'foo'
dasherize('foo_bar').should == 'foo-bar'
dasherize('foo/bar').should == 'foo-bar'
dasherize('foo/bar/index').should == 'foo-bar-index'
end
it 'should concat strings' do
concat('foo', 'bar').should == 'foobar'
concat('hello', 'foo', 'bar').should == 'hellofoobar'
end
it 'should return the input string every n occurences' do
modulo('foo', 0, 3).should == ''
modulo('foo', 1, 3).should == ''
modulo('foo', 2, 3).should == 'foo'
modulo('foo', 3, 3).should == ''
modulo('foo', 4, 3).should == ''
modulo('foo', 5, 3).should == 'foo'
end
end

View File

@ -0,0 +1,99 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::Paginate do
it 'should have a valid syntax' do
markup = "contents.projects by 5"
lambda do
Locomotive::Liquid::Tags::Paginate.new('paginate', markup, ["{% endpaginate %}"])
end.should_not raise_error
end
it 'should raise an error if the syntax is incorrect' do
["contents.projects by a", "contents.projects", "contents.projects 5"].each do |markup|
lambda do
Locomotive::Liquid::Tags::Paginate.new('paginate', markup, ["{% endpaginate %}"])
end.should raise_error
end
end
it 'should paginate the collection' do
template = Liquid::Template.parse(default_template)
text = template.render!(liquid_context)
text.should match /!Ruby on Rails!/
text.should match /!jQuery!/
text.should_not match /!mongodb!/
text = template.render!(liquid_context(:page => 2))
text.should_not match /!jQuery!/
text.should match /!mongodb!/
text.should match /!Liquid!/
text.should_not match /!sqlite3!/
end
it 'should not paginate if collection is nil or empty' do
template = Liquid::Template.parse(default_template)
lambda do
template.render!(liquid_context(:collection => nil))
end.should raise_error
lambda do
template.render!(liquid_context(:collection => PaginatedCollection.new))
end.should raise_error
end
# ___ helpers methods ___ #
def liquid_context(options = {})
::Liquid::Context.new({
'projects' => options.has_key?(:collection) ? options[:collection] : PaginatedCollection.new(['Ruby on Rails', 'jQuery', 'mongodb', 'Liquid', 'sqlite3']),
'current_page' => options[:page] || 1
}, {}, true)
end
def default_template
"{% paginate projects by 2 %}
{% for project in paginate.collection %}
!{{ project }}!
{% endfor %}
{% endpaginate %}"
end
class PaginatedCollection
def initialize(collection)
@collection = collection || []
end
def paginate(options = {})
total_pages = (@collection.size.to_f / options[:per_page].to_f).to_f.ceil + 1
offset = (options[:page] - 1) * options[:per_page]
{
:collection => @collection[offset..(offset + options[:per_page]) - 1],
:current_page => options[:page],
:previous_page => options[:page] == 1 ? 1 : options[:page] - 1,
:next_page => options[:page] == total_pages ? total_pages : options[:page] + 1,
:total_entries => @collection.size,
:total_pages => total_pages,
:per_page => options[:per_page]
}
end
def each(&block)
@collection.each(&block)
end
def method_missing(method, *args)
@collection.send(method, *args)
end
def to_liquid
self
end
end
end

View File

@ -0,0 +1,20 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::Snippet do
before(:each) do
Site.any_instance.stubs(:create_default_pages!).returns(true)
site = Factory.build(:site)
snippet = Factory.build(:snippet, :site => site)
snippet.send(:store_template)
site.snippets.stubs(:where).returns([snippet])
@context = ::Liquid::Context.new({}, { :site => site })
end
it 'should render it' do
template = ::Liquid::Template.parse("{% include 'header' %}")
text = template.render(@context)
text.should == "<title>Acme</title>"
end
end

View File

@ -0,0 +1,20 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::WithScope do
it 'should decode options (boolean, interger, ...)' do
scope = Locomotive::Liquid::Tags::WithScope.new('with_scope', 'active:true price:42 title:\'foo\' hidden:false', ["{% endwith_scope %}"])
attributes = scope.send(:decode, scope.instance_variable_get(:@attributes))
attributes['active'].should == true
attributes['price'].should == 42
attributes['title'].should == 'foo'
attributes['hidden'].should == false
end
it 'should store attributes in the context' do
template = ::Liquid::Template.parse("{% with_scope active:true title:'foo' %}{{ with_scope.active }}-{{ with_scope.title }}{% endwith_scope %}")
text = template.render
text.should == "true-foo"
end
end

View File

@ -0,0 +1,57 @@
require 'spec_helper'
describe 'Locomotive rendering system' do
before(:each) do
@controller = Locomotive::TestController.new
Site.any_instance.stubs(:create_default_pages!).returns(true)
@site = Factory.build(:site)
Site.stubs(:find).returns(@site)
@controller.current_site = @site
end
context 'setting the response' do
before(:each) do
@controller.send(:prepare_and_set_response, 'Hello world !')
end
it 'should have a html content type' do
@controller.response.headers["Content-Type"].should == 'text/html; charset=utf-8'
end
it 'should display the output' do
@controller.output.should == 'Hello world !'
end
end
context 'when retrieving page' do
it 'should retrieve the index page /' do
@controller.request.fullpath = '/'
@controller.current_site.pages.expects(:where).with({ :fullpath => 'index' }).returns([true])
@controller.send(:locomotive_page).should be_true
end
it 'should also retrieve the index page (index.html)' do
@controller.request.fullpath = '/index.html'
@controller.current_site.pages.expects(:where).with({ :fullpath => 'index' }).returns([true])
@controller.send(:locomotive_page).should be_true
end
it 'should retrieve it based on the full path' do
@controller.request.fullpath = '/about_us/team.html'
@controller.current_site.pages.expects(:where).with({ :fullpath => 'about_us/team' }).returns([true])
@controller.send(:locomotive_page).should be_true
end
it 'should return the 404 page if the page does not exist' do
@controller.request.fullpath = '/contact'
@controller.current_site.pages.expects(:not_found).returns([true])
@controller.send(:locomotive_page).should be_true
end
end
end

View File

@ -14,7 +14,7 @@ describe Layout do
layout.errors[:value].should == ["should contain 'content_for_layout' liquid tag"]
end
describe 'page parts' do
context 'dealing with page parts' do
before(:each) do
@layout = Factory.build(:layout)
@ -49,4 +49,16 @@ describe Layout do
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

View File

@ -248,15 +248,15 @@ describe Page do
archives.children.last.children.first.depth.should == 3
end
it 'should generate a route / url from parents' do
@home.route.should == 'index'
it 'should generate a path / url from parents' do
@home.fullpath.should == 'index'
@home.url.should == 'http://acme.example.com/index.html'
@child_1.route.should == 'foo'
@child_1.fullpath.should == 'foo'
@child_1.url.should == 'http://acme.example.com/foo.html'
nested_page = Factory(:page, :title => 'Sub sub page 1', :slug => 'bar', :parent => @child_1, :site => @home.site)
nested_page.route.should == 'foo/bar'
nested_page.fullpath.should == 'foo/bar'
nested_page.url.should == 'http://acme.example.com/foo/bar.html'
end
@ -286,5 +286,44 @@ describe Page do
[@child_1, @child_3.reload].each_with_index { |c, i| c.position.should == i + 1 }
end
end
end
context 'rendering' do
before(:each) do
@page = Factory.build(:page, :site => nil)
@page.parts.build :slug => 'layout', :value => 'Hello world !'
@page.parts.build :slug => 'sidebar', :value => 'A sidebar...'
@page.send(:store_template)
@layout = Factory.build(:layout, :site => nil)
@layout.send(:store_template)
@context = Liquid::Context.new
end
context 'without layout' do
it 'should render the body part' do
@page.render(@context).should == 'Hello world !'
end
end
context 'with layout' do
it 'should render both the body and sidebar parts' do
@page.layout = @layout
@page.render(@context).should == %{<html>
<head>
<title>My website</title>
</head>
<body>
<div id="sidebar">A sidebar...</div>
<div id="main">Hello world !</div>
</body>
</html>}
end
end
end
end

View File

@ -1,3 +1,40 @@
Locomotive.configure do |config|
config.default_domain = 'example.com'
end
module Locomotive
class TestController
include Locomotive::Render
attr_accessor :output, :current_site
def render(options = {})
self.output = options[:text]
end
def response
@response ||= TestResponse.new
end
def request
@request ||= TestRequest.new
end
end
class TestResponse
attr_accessor :headers
def initialize
self.headers = {}
end
end
class TestRequest
attr_accessor :fullpath
end
end

View File

@ -23,7 +23,7 @@ module Mongoid #:nodoc:
alias_method_chain :parentize, :custom_fields
def custom_fields_association_name(association_name)
"#{association_name.singularize}_custom_fields".to_sym
"#{association_name.to_s.singularize}_custom_fields".to_sym
end
def custom_fields?(object, association_name)