aslakjo-comatose 2.0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +195 -0
- data/INSTALL +20 -0
- data/LICENSE +20 -0
- data/MANIFEST +91 -0
- data/README.markdown +159 -0
- data/Rakefile +176 -0
- data/SPECS +61 -0
- data/about.yml +7 -0
- data/bin/comatose +112 -0
- data/comatose.gemspec +113 -0
- data/generators/comatose_migration/USAGE +15 -0
- data/generators/comatose_migration/comatose_migration_generator.rb +74 -0
- data/generators/comatose_migration/templates/migration.rb +35 -0
- data/generators/comatose_migration/templates/v4_upgrade.rb +15 -0
- data/generators/comatose_migration/templates/v6_upgrade.rb +23 -0
- data/generators/comatose_migration/templates/v7_upgrade.rb +22 -0
- data/init.rb +2 -0
- data/install.rb +18 -0
- data/lib/acts_as_versioned.rb +543 -0
- data/lib/comatose/comatose_drop.rb +79 -0
- data/lib/comatose/configuration.rb +69 -0
- data/lib/comatose/page_wrapper.rb +119 -0
- data/lib/comatose/processing_context.rb +69 -0
- data/lib/comatose/tasks/admin.rb +60 -0
- data/lib/comatose/tasks/data.rb +82 -0
- data/lib/comatose/tasks/setup.rb +52 -0
- data/lib/comatose/version.rb +4 -0
- data/lib/comatose.rb +33 -0
- data/lib/comatose_admin_controller.rb +395 -0
- data/lib/comatose_admin_helper.rb +37 -0
- data/lib/comatose_controller.rb +138 -0
- data/lib/comatose_helper.rb +3 -0
- data/lib/comatose_page.rb +141 -0
- data/lib/liquid/block.rb +96 -0
- data/lib/liquid/context.rb +190 -0
- data/lib/liquid/document.rb +17 -0
- data/lib/liquid/drop.rb +48 -0
- data/lib/liquid/errors.rb +7 -0
- data/lib/liquid/extensions.rb +53 -0
- data/lib/liquid/file_system.rb +62 -0
- data/lib/liquid/htmltags.rb +64 -0
- data/lib/liquid/standardfilters.rb +111 -0
- data/lib/liquid/standardtags.rb +399 -0
- data/lib/liquid/strainer.rb +42 -0
- data/lib/liquid/tag.rb +25 -0
- data/lib/liquid/template.rb +88 -0
- data/lib/liquid/variable.rb +39 -0
- data/lib/liquid.rb +52 -0
- data/lib/redcloth.rb +1129 -0
- data/lib/support/class_options.rb +36 -0
- data/lib/support/inline_rendering.rb +48 -0
- data/lib/support/route_mapper.rb +50 -0
- data/lib/text_filters/markdown.rb +14 -0
- data/lib/text_filters/markdown_smartypants.rb +15 -0
- data/lib/text_filters/none.rb +8 -0
- data/lib/text_filters/rdoc.rb +13 -0
- data/lib/text_filters/simple.rb +8 -0
- data/lib/text_filters/textile.rb +15 -0
- data/lib/text_filters.rb +140 -0
- data/rails/init.rb +3 -0
- data/resources/layouts/comatose_admin_template.html.erb +28 -0
- data/resources/public/images/collapsed.gif +0 -0
- data/resources/public/images/expanded.gif +0 -0
- data/resources/public/images/no-children.gif +0 -0
- data/resources/public/images/page.gif +0 -0
- data/resources/public/images/spinner.gif +0 -0
- data/resources/public/images/title-hover-bg.gif +0 -0
- data/resources/public/javascripts/comatose_admin.js +401 -0
- data/resources/public/stylesheets/comatose_admin.css +404 -0
- data/tasks/comatose.rake +9 -0
- data/test/behaviors.rb +106 -0
- data/test/fixtures/comatose_pages.yml +96 -0
- data/test/functional/comatose_admin_controller_test.rb +114 -0
- data/test/functional/comatose_controller_test.rb +44 -0
- data/test/javascripts/test.html +26 -0
- data/test/javascripts/test_runner.js +307 -0
- data/test/test_helper.rb +55 -0
- data/test/unit/class_options_test.rb +52 -0
- data/test/unit/comatose_page_test.rb +136 -0
- data/test/unit/processing_context_test.rb +108 -0
- data/test/unit/text_filters_test.rb +52 -0
- data/views/comatose_admin/_form.html.erb +96 -0
- data/views/comatose_admin/_page_list_item.html.erb +60 -0
- data/views/comatose_admin/delete.html.erb +18 -0
- data/views/comatose_admin/edit.html.erb +5 -0
- data/views/comatose_admin/index.html.erb +29 -0
- data/views/comatose_admin/new.html.erb +5 -0
- data/views/comatose_admin/reorder.html.erb +30 -0
- data/views/comatose_admin/versions.html.erb +40 -0
- data/views/layouts/comatose_admin.html.erb +837 -0
- data/views/layouts/comatose_admin_customize.html.erb +28 -0
- data/views/layouts/comatose_content.html.erb +17 -0
- metadata +148 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# ComatosePage attributes
|
2
|
+
# - parent_id
|
3
|
+
# - title
|
4
|
+
# - full_path
|
5
|
+
# - slug
|
6
|
+
# - keywords
|
7
|
+
# - body
|
8
|
+
# - author
|
9
|
+
# - filter_type
|
10
|
+
# - position
|
11
|
+
# - version
|
12
|
+
# - updated_on
|
13
|
+
# - created_on
|
14
|
+
class ComatosePage < ActiveRecord::Base
|
15
|
+
|
16
|
+
set_table_name 'comatose_pages'
|
17
|
+
|
18
|
+
# Only versions the content... Not all of the meta data or position
|
19
|
+
acts_as_versioned :table_name=>'comatose_page_versions', :if_changed => [:title, :slug, :keywords, :body]
|
20
|
+
|
21
|
+
define_option :active_mount_info, {:root=>'', :index=>''}
|
22
|
+
|
23
|
+
acts_as_tree :order => "position, title"
|
24
|
+
acts_as_list :scope => :parent_id
|
25
|
+
|
26
|
+
#before_create :create_full_path
|
27
|
+
before_save :cache_full_path, :create_full_path
|
28
|
+
after_save :update_children_full_path
|
29
|
+
|
30
|
+
# Using before_validation so we can default the slug from the title
|
31
|
+
before_validation do |record|
|
32
|
+
# Create slug from title
|
33
|
+
if record.slug.blank? and !record.title.blank?
|
34
|
+
record.slug = record.title.downcase.lstrip.rstrip.gsub( /[^-a-z0-9~\s\.:;+=_]/, '').gsub(/[\s\.:;=_+]+/, '-').gsub(/[\-]{2,}/, '-').to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Manually set these, because record_timestamps = false
|
39
|
+
before_create do |record|
|
40
|
+
record.created_on = record.updated_on = Time.now
|
41
|
+
end
|
42
|
+
|
43
|
+
validates_presence_of :title, :on => :save, :message => "must be present"
|
44
|
+
validates_uniqueness_of :slug, :on => :save, :scope=>'parent_id', :message => "is already in use"
|
45
|
+
validates_presence_of :parent_id, :on=>:create, :message=>"must be present"
|
46
|
+
|
47
|
+
# Tests ERB/Liquid content...
|
48
|
+
validates_each :body, :allow_nil=>true, :allow_blank=>true do |record, attr, value|
|
49
|
+
begin
|
50
|
+
body_html = record.to_html
|
51
|
+
rescue SyntaxError
|
52
|
+
record.errors.add :body, "syntax error: #{$!.to_s.gsub('<', '<')}"
|
53
|
+
rescue
|
54
|
+
record.errors.add :body, "content error: #{$!.to_s.gsub('<', '<')}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a pages URI dynamically, based on the active mount point
|
59
|
+
def uri
|
60
|
+
if full_path == ''
|
61
|
+
active_mount_info[:root]
|
62
|
+
else
|
63
|
+
page_path = (full_path || '').split('/')
|
64
|
+
idx_path = active_mount_info[:index].split('/')
|
65
|
+
uri_root = active_mount_info[:root].split('/')
|
66
|
+
uri_path = ( uri_root + (page_path - idx_path) ).flatten.delete_if {|i| i == "" }
|
67
|
+
['',uri_path].join('/')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if a page has a selected keyword... NOT case sensitive.
|
72
|
+
# So the keyword McCray is the same as mccray
|
73
|
+
def has_keyword?(keyword)
|
74
|
+
@key_list ||= (self.keywords || '').downcase.split(',').map {|k| k.strip }
|
75
|
+
@key_list.include? keyword.to_s.downcase
|
76
|
+
rescue
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the page's content, transformed and filtered...
|
81
|
+
def to_html(options={})
|
82
|
+
#version = options.delete(:version)
|
83
|
+
text = self.body
|
84
|
+
binding = Comatose::ProcessingContext.new(self, options)
|
85
|
+
filter_type = self.filter_type || Comatose.config.default_filter
|
86
|
+
TextFilters.transform(text, binding, filter_type, Comatose.config.default_processor)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Static helpers...
|
90
|
+
|
91
|
+
# Returns a Page with a matching path.
|
92
|
+
def self.find_by_path( path )
|
93
|
+
path = path.split('.')[0] unless path.empty? # Will ignore file extension...
|
94
|
+
path = path[1..-1] if path.starts_with? "/"
|
95
|
+
find( :first, :conditions=>[ 'full_path = ?', path ] )
|
96
|
+
end
|
97
|
+
|
98
|
+
# Overrides...
|
99
|
+
|
100
|
+
# I don't want the AR magic timestamping support for this class...
|
101
|
+
def record_timestamps
|
102
|
+
false
|
103
|
+
end
|
104
|
+
def self.record_timestamps
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
# Creates a URI path based on the Page tree
|
111
|
+
def create_full_path
|
112
|
+
if parent_node = self.parent
|
113
|
+
# Build URI Path
|
114
|
+
path = "#{parent_node.full_path}/#{self.slug}"
|
115
|
+
# strip leading space, if there is one...
|
116
|
+
path = path[1..-1] if path.starts_with? "/"
|
117
|
+
self.full_path = path || ""
|
118
|
+
else
|
119
|
+
# I'm the root -- My path is blank
|
120
|
+
self.full_path = ""
|
121
|
+
end
|
122
|
+
end
|
123
|
+
def create_full_path!
|
124
|
+
create_full_path
|
125
|
+
save
|
126
|
+
end
|
127
|
+
|
128
|
+
# Caches old path (before save) for comparison later
|
129
|
+
def cache_full_path
|
130
|
+
@old_full_path = self.full_path
|
131
|
+
end
|
132
|
+
|
133
|
+
# Updates all this content's child URI paths
|
134
|
+
def update_children_full_path(should_save=true)
|
135
|
+
# OPTIMIZE: Only update all the children if the :slug/:fullpath is different
|
136
|
+
for child in self.children
|
137
|
+
child.create_full_path! unless child.frozen?
|
138
|
+
child.update_children_full_path(should_save)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/liquid/block.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
class Block < Tag
|
4
|
+
def parse(tokens)
|
5
|
+
@nodelist ||= []
|
6
|
+
@nodelist.clear
|
7
|
+
|
8
|
+
while token = tokens.shift
|
9
|
+
|
10
|
+
case token
|
11
|
+
when /^#{TagStart}/
|
12
|
+
if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
|
13
|
+
|
14
|
+
# if we found the proper block delimitor just end parsing here and let the outer block
|
15
|
+
# proceed
|
16
|
+
if block_delimiter == $1
|
17
|
+
end_tag
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
# fetch the tag from registered blocks
|
22
|
+
if tag = Template.tags[$1]
|
23
|
+
@nodelist << tag.new($2, tokens)
|
24
|
+
else
|
25
|
+
# this tag is not registered with the system
|
26
|
+
# pass it to the current block for special handling or error reporting
|
27
|
+
unknown_tag($1, $2, tokens)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
|
31
|
+
end
|
32
|
+
when /^#{VariableStart}/
|
33
|
+
@nodelist << create_variable(token)
|
34
|
+
when ''
|
35
|
+
# pass
|
36
|
+
else
|
37
|
+
@nodelist << token
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Make sure that its ok to end parsing in the current block.
|
42
|
+
# Effectively this method will throw and exception unless the current block is
|
43
|
+
# of type Document
|
44
|
+
assert_missing_delimitation!
|
45
|
+
end
|
46
|
+
|
47
|
+
def end_tag
|
48
|
+
end
|
49
|
+
|
50
|
+
def unknown_tag(tag, params, tokens)
|
51
|
+
case tag
|
52
|
+
when 'else'
|
53
|
+
raise SyntaxError, "#{block_name} tag does not expect else tag"
|
54
|
+
when 'end'
|
55
|
+
raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
|
56
|
+
else
|
57
|
+
raise SyntaxError, "Unknown tag '#{tag}'"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def block_delimiter
|
62
|
+
"end#{block_name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def block_name
|
66
|
+
self.class.name.scan(/\w+$/).first.downcase
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_variable(token)
|
70
|
+
token.scan(/^#{VariableStart}(.*)#{VariableEnd}$/) do |content|
|
71
|
+
return Variable.new(content.first)
|
72
|
+
end
|
73
|
+
raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
|
74
|
+
end
|
75
|
+
|
76
|
+
def render(context)
|
77
|
+
render_all(@nodelist, context)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def assert_missing_delimitation!
|
83
|
+
raise SyntaxError.new("#{block_name} tag was never closed")
|
84
|
+
end
|
85
|
+
|
86
|
+
def render_all(list, context)
|
87
|
+
list.collect do |token|
|
88
|
+
if token.respond_to?(:render)
|
89
|
+
token.render(context)
|
90
|
+
else
|
91
|
+
token.to_s
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
class ContextError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Context keeps the variable stack and resolves variables, as well as keywords
|
7
|
+
#
|
8
|
+
# context['variable'] = 'testing'
|
9
|
+
# context['variable'] #=> 'testing'
|
10
|
+
# context['true'] #=> true
|
11
|
+
# context['10.2232'] #=> 10.2232
|
12
|
+
#
|
13
|
+
# context.stack do
|
14
|
+
# context['bob'] = 'bobsen'
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# context['bob'] #=> nil class Context
|
18
|
+
class Context
|
19
|
+
attr_reader :assigns
|
20
|
+
attr_accessor :registers
|
21
|
+
|
22
|
+
def initialize(assigns = {}, registers = nil)
|
23
|
+
@assigns = [assigns]
|
24
|
+
@registers = registers || {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def strainer
|
28
|
+
@strainer ||= Strainer.create(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
# adds filters to this context.
|
32
|
+
# this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
|
33
|
+
# for that
|
34
|
+
def add_filters(filter)
|
35
|
+
return unless filter.is_a?(Module)
|
36
|
+
strainer.extend(filter)
|
37
|
+
end
|
38
|
+
|
39
|
+
def invoke(method, *args)
|
40
|
+
if strainer.respond_to?(method)
|
41
|
+
strainer.__send__(method, *args)
|
42
|
+
else
|
43
|
+
return args[0]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# push new local scope on the stack. use <tt>Context#stack</tt> instead
|
48
|
+
def push
|
49
|
+
@assigns.unshift({})
|
50
|
+
end
|
51
|
+
|
52
|
+
# merge a hash of variables in the current local scope
|
53
|
+
def merge(new_assigns)
|
54
|
+
@assigns[0].merge!(new_assigns)
|
55
|
+
end
|
56
|
+
|
57
|
+
# pop from the stack. use <tt>Context#stack</tt> instead
|
58
|
+
def pop
|
59
|
+
raise ContextError if @assigns.size == 1
|
60
|
+
@assigns.shift
|
61
|
+
end
|
62
|
+
|
63
|
+
# pushes a new local scope on the stack, pops it at the end of the block
|
64
|
+
#
|
65
|
+
# Example:
|
66
|
+
#
|
67
|
+
# context.stack do
|
68
|
+
# context['var'] = 'hi'
|
69
|
+
# end
|
70
|
+
# context['var] #=> nil
|
71
|
+
#
|
72
|
+
def stack(&block)
|
73
|
+
push
|
74
|
+
begin
|
75
|
+
result = yield
|
76
|
+
ensure
|
77
|
+
pop
|
78
|
+
end
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
# Only allow String, Numeric, Hash, Array or <tt>Liquid::Drop</tt>
|
83
|
+
def []=(key, value)
|
84
|
+
@assigns[0][key] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
def [](key)
|
88
|
+
resolve(key)
|
89
|
+
end
|
90
|
+
|
91
|
+
def has_key?(key)
|
92
|
+
resolve(key) != nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
98
|
+
# Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
|
99
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
100
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
101
|
+
#
|
102
|
+
# Example:
|
103
|
+
#
|
104
|
+
# products == empty #=> products.empty?
|
105
|
+
#
|
106
|
+
def resolve(key)
|
107
|
+
case key
|
108
|
+
when nil
|
109
|
+
nil
|
110
|
+
when 'true'
|
111
|
+
true
|
112
|
+
when 'false'
|
113
|
+
false
|
114
|
+
when 'empty'
|
115
|
+
:empty?
|
116
|
+
when 'nil', 'null'
|
117
|
+
nil
|
118
|
+
# Single quoted strings
|
119
|
+
when /^'(.*)'$/
|
120
|
+
$1.to_s
|
121
|
+
# Double quoted strings
|
122
|
+
when /^"(.*)"$/
|
123
|
+
$1.to_s
|
124
|
+
# Integer and floats
|
125
|
+
when /^(\d+)$/
|
126
|
+
$1.to_i
|
127
|
+
when /^(\d[\d\.]+)$/
|
128
|
+
$1.to_f
|
129
|
+
else
|
130
|
+
variable(key)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# fetches an object starting at the local scope and then moving up
|
135
|
+
# the hierachy
|
136
|
+
def fetch(key)
|
137
|
+
begin
|
138
|
+
for scope in @assigns
|
139
|
+
if scope.has_key?(key)
|
140
|
+
obj = scope[key]
|
141
|
+
if obj.is_a?(Liquid::Drop)
|
142
|
+
obj.context = self
|
143
|
+
end
|
144
|
+
return obj
|
145
|
+
end
|
146
|
+
end
|
147
|
+
rescue => e
|
148
|
+
raise ContextError, "Could not fetch key #{key} from context: " + e.message
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
# resolves namespaced queries gracefully.
|
154
|
+
#
|
155
|
+
# Example
|
156
|
+
#
|
157
|
+
# @context['hash'] = {"name" => 'tobi'}
|
158
|
+
# assert_equal 'tobi', @context['hash.name']
|
159
|
+
def variable(key)
|
160
|
+
parts = key.split(VariableAttributeSeparator)
|
161
|
+
|
162
|
+
|
163
|
+
if object = fetch(parts.shift).to_liquid
|
164
|
+
object.context = self if object.is_a?(Liquid::Drop)
|
165
|
+
|
166
|
+
while not parts.size.zero?
|
167
|
+
next_part_name = parts.shift
|
168
|
+
|
169
|
+
# If the last part of the context variable is .size we just
|
170
|
+
# return the count of the objects in this object
|
171
|
+
if next_part_name == 'size' and parts.empty?
|
172
|
+
return object.size if object.is_a?(Array)
|
173
|
+
return object.size if object.is_a?(Hash) && !object.has_key?(next_part_name)
|
174
|
+
end
|
175
|
+
|
176
|
+
return nil if not object.respond_to?(:has_key?)
|
177
|
+
return nil if not object.has_key?(next_part_name)
|
178
|
+
|
179
|
+
object = object[next_part_name].to_liquid
|
180
|
+
object.context = self if object.is_a?(Liquid::Drop)
|
181
|
+
end
|
182
|
+
|
183
|
+
object
|
184
|
+
else
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Document < Block
|
3
|
+
# we don't need markup to open this block
|
4
|
+
def initialize(tokens)
|
5
|
+
parse(tokens)
|
6
|
+
end
|
7
|
+
|
8
|
+
# There isn't a real delimter
|
9
|
+
def block_delimiter
|
10
|
+
[]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Document blocks don't need to be terminated since they are not actually opened
|
14
|
+
def assert_missing_delimitation!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/liquid/drop.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# A drop in liquid is a class which allows you to to export DOM like things to liquid
|
4
|
+
# Methods of drops are callable.
|
5
|
+
# The main use for liquid drops is the implement lazy loaded objects.
|
6
|
+
# If you would like to make data available to the web designers which you don't want loaded unless needed then
|
7
|
+
# a drop is a great way to do that
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# class ProductDrop < Liquid::Drop
|
12
|
+
# def top_sales
|
13
|
+
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
18
|
+
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
19
|
+
#
|
20
|
+
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
|
21
|
+
# catch all
|
22
|
+
class Drop
|
23
|
+
attr_writer :context
|
24
|
+
|
25
|
+
# Catch all for the method
|
26
|
+
def before_method(method)
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# called by liquid to invoke a drop
|
31
|
+
def invoke_drop(method)
|
32
|
+
result = before_method(method)
|
33
|
+
result ||= send(method.to_sym) if self.class.public_instance_methods.include?(method.to_s)
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_key?(name)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_liquid
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
alias :[] :invoke_drop
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class String
|
2
|
+
def to_liquid
|
3
|
+
self
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class Array
|
8
|
+
def to_liquid
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Hash
|
14
|
+
def to_liquid
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Numeric
|
20
|
+
def to_liquid
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Time
|
26
|
+
def to_liquid
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class DateTime
|
32
|
+
def to_liquid
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Date
|
38
|
+
def to_liquid
|
39
|
+
self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def true.to_liquid
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def false.to_liquid
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def nil.to_liquid
|
52
|
+
self
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Liquid
|
2
|
+
# A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
|
3
|
+
#
|
4
|
+
# You can implement subclasses that retrieve templates from the database, from the file system using a different
|
5
|
+
# path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
|
6
|
+
#
|
7
|
+
# You can add additional instance variables, arguments, or methods as needed.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
12
|
+
# liquid = Liquid::Template.parse(template)
|
13
|
+
#
|
14
|
+
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
15
|
+
class BlankFileSystem
|
16
|
+
# Called by Liquid to retrieve a template file
|
17
|
+
def read_template_file(template_path)
|
18
|
+
raise FileSystemError, "This liquid context does not allow includes."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
|
23
|
+
# ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
|
24
|
+
#
|
25
|
+
# For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
|
26
|
+
#
|
27
|
+
# Example:
|
28
|
+
#
|
29
|
+
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
30
|
+
#
|
31
|
+
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
32
|
+
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
33
|
+
#
|
34
|
+
class LocalFileSystem
|
35
|
+
attr_accessor :root
|
36
|
+
|
37
|
+
def initialize(root)
|
38
|
+
@root = root
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_template_file(template_path)
|
42
|
+
full_path = full_path(template_path)
|
43
|
+
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
|
44
|
+
|
45
|
+
File.read(full_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def full_path(template_path)
|
49
|
+
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
|
50
|
+
|
51
|
+
full_path = if template_path.include?('/')
|
52
|
+
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
|
53
|
+
else
|
54
|
+
File.join(root, "_#{template_path}.liquid")
|
55
|
+
end
|
56
|
+
|
57
|
+
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
|
58
|
+
|
59
|
+
full_path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Liquid
|
2
|
+
class TableRow < Block
|
3
|
+
Syntax = /(\w+)\s+in\s+(#{AllowedVariableCharacters}+)/
|
4
|
+
|
5
|
+
def initialize(markup, tokens)
|
6
|
+
super
|
7
|
+
|
8
|
+
if markup =~ Syntax
|
9
|
+
@variable_name = $1
|
10
|
+
@collection_name = $2
|
11
|
+
@attributes = {}
|
12
|
+
markup.scan(TagAttributes) do |key, value|
|
13
|
+
@attributes[key] = value
|
14
|
+
end
|
15
|
+
else
|
16
|
+
raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(context)
|
21
|
+
collection = context[@collection_name] or return ''
|
22
|
+
|
23
|
+
if @attributes['limit'] or @attributes['offset']
|
24
|
+
limit = context[@attributes['limit']] || -1
|
25
|
+
offset = context[@attributes['offset']] || 0
|
26
|
+
collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
|
27
|
+
end
|
28
|
+
|
29
|
+
length = collection.length
|
30
|
+
|
31
|
+
cols = context[@attributes['cols']].to_i
|
32
|
+
|
33
|
+
row = 1
|
34
|
+
col = 0
|
35
|
+
|
36
|
+
result = ["<tr class=\"row1\">\n"]
|
37
|
+
context.stack do
|
38
|
+
|
39
|
+
collection.each_with_index do |item, index|
|
40
|
+
context[@variable_name] = item
|
41
|
+
context['tablerowloop'] = {
|
42
|
+
'length' => length,
|
43
|
+
'index' => index + 1,
|
44
|
+
'index0' => index,
|
45
|
+
'rindex' => length - index,
|
46
|
+
'rindex0' => length - index -1,
|
47
|
+
'first' => (index == 0),
|
48
|
+
'last' => (index == length - 1) }
|
49
|
+
|
50
|
+
result << ["<td class=\"col#{col += 1}\">"] + render_all(@nodelist, context) + ['</td>']
|
51
|
+
|
52
|
+
if col == cols and not (index == length - 1)
|
53
|
+
col = 0
|
54
|
+
result << ["</tr>\n<tr class=\"row#{row += 1}\">"]
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
result + ["</tr>\n"]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Template.register_tag('tablerow', TableRow)
|
64
|
+
end
|