qoobaa-liquid 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,232 @@
1
+ module Liquid
2
+
3
+ # Context keeps the variable stack and resolves variables, as well as keywords
4
+ #
5
+ # context['variable'] = 'testing'
6
+ # context['variable'] #=> 'testing'
7
+ # context['true'] #=> true
8
+ # context['10.2232'] #=> 10.2232
9
+ #
10
+ # context.stack do
11
+ # context['bob'] = 'bobsen'
12
+ # end
13
+ #
14
+ # context['bob'] #=> nil class Context
15
+ class Context
16
+ attr_reader :scopes
17
+ attr_reader :errors, :registers
18
+
19
+ def initialize(assigns = {}, registers = {}, rethrow_errors = false)
20
+ @scopes = [(assigns || {})]
21
+ @registers = registers
22
+ @errors = []
23
+ @rethrow_errors = rethrow_errors
24
+ end
25
+
26
+ def strainer
27
+ @strainer ||= Strainer.create(self)
28
+ end
29
+
30
+ # adds filters to this context.
31
+ # this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
32
+ # for that
33
+ def add_filters(filters)
34
+ filters = [filters].flatten.compact
35
+
36
+ filters.each do |f|
37
+ raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
38
+ strainer.extend(f)
39
+ end
40
+ end
41
+
42
+ def handle_error(e)
43
+ errors.push(e)
44
+ raise if @rethrow_errors
45
+
46
+ case e
47
+ when SyntaxError
48
+ "Liquid syntax error: #{e.message}"
49
+ else
50
+ "Liquid error: #{e.message}"
51
+ end
52
+ end
53
+
54
+
55
+ def invoke(method, *args)
56
+ if strainer.respond_to?(method)
57
+ strainer.__send__(method, *args)
58
+ else
59
+ args.first
60
+ end
61
+ end
62
+
63
+ # push new local scope on the stack. use <tt>Context#stack</tt> instead
64
+ def push
65
+ raise StackLevelError, "Nesting too deep" if @scopes.length > 100
66
+ @scopes.unshift({})
67
+ end
68
+
69
+ # merge a hash of variables in the current local scope
70
+ def merge(new_scopes)
71
+ @scopes[0].merge!(new_scopes)
72
+ end
73
+
74
+ # pop from the stack. use <tt>Context#stack</tt> instead
75
+ def pop
76
+ raise ContextError if @scopes.size == 1
77
+ @scopes.shift
78
+ end
79
+
80
+ # pushes a new local scope on the stack, pops it at the end of the block
81
+ #
82
+ # Example:
83
+ #
84
+ # context.stack do
85
+ # context['var'] = 'hi'
86
+ # end
87
+ # context['var] #=> nil
88
+ #
89
+ def stack(&block)
90
+ result = nil
91
+ push
92
+ begin
93
+ result = yield
94
+ ensure
95
+ pop
96
+ end
97
+ result
98
+ end
99
+
100
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
101
+ def []=(key, value)
102
+ @scopes[0][key] = value
103
+ end
104
+
105
+ def [](key)
106
+ resolve(key)
107
+ end
108
+
109
+ def has_key?(key)
110
+ resolve(key) != nil
111
+ end
112
+
113
+ private
114
+
115
+ # Look up variable, either resolve directly after considering the name. We can directly handle
116
+ # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
117
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
118
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
119
+ #
120
+ # Example:
121
+ #
122
+ # products == empty #=> products.empty?
123
+ #
124
+ def resolve(key)
125
+ case key
126
+ when nil, 'nil', 'null', ''
127
+ nil
128
+ when 'true'
129
+ true
130
+ when 'false'
131
+ false
132
+ when 'blank'
133
+ :blank?
134
+ when 'empty'
135
+ :empty?
136
+ # filtered variables
137
+ when SpacelessFilter
138
+ filtered_variable(key)
139
+ # Single quoted strings
140
+ when /^'(.*)'$/
141
+ $1.to_s
142
+ # Double quoted strings
143
+ when /^"(.*)"$/
144
+ $1.to_s
145
+ # Integer and floats
146
+ when /^(\d+)$/
147
+ $1.to_i
148
+ # Ranges
149
+ when /^\((\S+)\.\.(\S+)\)$/
150
+ (resolve($1).to_i..resolve($2).to_i)
151
+ # Floats
152
+ when /^(\d[\d\.]+)$/
153
+ $1.to_f
154
+ else
155
+ variable(key)
156
+ end
157
+ end
158
+
159
+ # fetches an object starting at the local scope and then moving up
160
+ # the hierachy
161
+ def find_variable(key)
162
+ @scopes.each do |scope|
163
+ if scope.has_key?(key)
164
+ variable = scope[key]
165
+ variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
166
+ variable = variable.to_liquid
167
+ variable.context = self if variable.respond_to?(:context=)
168
+ return variable
169
+ end
170
+ end
171
+ nil
172
+ end
173
+
174
+ # resolves namespaced queries gracefully.
175
+ #
176
+ # Example
177
+ #
178
+ # @context['hash'] = {"name" => 'tobi'}
179
+ # assert_equal 'tobi', @context['hash.name']
180
+ # assert_equal 'tobi', @context['hash["name"]']
181
+ #
182
+ def variable(markup)
183
+ parts = markup.scan(VariableParser)
184
+ square_bracketed = /^\[(.*)\]$/
185
+
186
+ first_part = parts.shift
187
+ if first_part =~ square_bracketed
188
+ first_part = resolve($1)
189
+ end
190
+
191
+ if object = find_variable(first_part)
192
+
193
+ parts.each do |part|
194
+ part = resolve($1) if part_resolved = (part =~ square_bracketed)
195
+
196
+ # If object is a hash- or array-like object we look for the
197
+ # presence of the key and if its available we return it
198
+ if object.respond_to?(:[]) and
199
+ ((object.respond_to?(:has_key?) and object.has_key?(part)) or
200
+ (object.respond_to?(:fetch) and part.is_a?(Integer)))
201
+
202
+ # if its a proc we will replace the entry with the proc
203
+ res = object[part]
204
+ res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
205
+ object = res.to_liquid
206
+
207
+ # Some special cases. If the part wasn't in square brackets and
208
+ # no key with the same name was found we interpret following calls
209
+ # as commands and call them on the current object
210
+ elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
211
+
212
+ object = object.send(part.intern).to_liquid
213
+
214
+ # No key was present with the desired value and it wasn't one of the directly supported
215
+ # keywords either. The only thing we got left is to return nil
216
+ else
217
+ return nil
218
+ end
219
+
220
+ # If we are dealing with a drop here we have to
221
+ object.context = self if object.respond_to?(:context=)
222
+ end
223
+ end
224
+
225
+ object
226
+ end
227
+
228
+ def filtered_variable(markup)
229
+ Variable.new(markup).render(self)
230
+ end
231
+ end
232
+ 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
@@ -0,0 +1,50 @@
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
+ if self.class.public_instance_methods.include?(method.to_s)
33
+ send(method.to_sym)
34
+ else
35
+ before_method(method)
36
+ end
37
+ end
38
+
39
+ def has_key?(name)
40
+ true
41
+ end
42
+
43
+ def to_liquid
44
+ self
45
+ end
46
+
47
+ alias :[] :invoke_drop
48
+ end
49
+
50
+ end
@@ -0,0 +1,11 @@
1
+ module Liquid
2
+ class Error < ::StandardError; end
3
+
4
+ class ArgumentError < Error; end
5
+ class ContextError < Error; end
6
+ class FilterNotFound < Error; end
7
+ class FileSystemError < Error; end
8
+ class StandardError < Error; end
9
+ class SyntaxError < Error; end
10
+ class StackLevelError < Error; end
11
+ end
@@ -0,0 +1,56 @@
1
+ require 'time'
2
+ require 'date'
3
+
4
+ class String # :nodoc:
5
+ def to_liquid
6
+ self
7
+ end
8
+ end
9
+
10
+ class Array # :nodoc:
11
+ def to_liquid
12
+ self
13
+ end
14
+ end
15
+
16
+ class Hash # :nodoc:
17
+ def to_liquid
18
+ self
19
+ end
20
+ end
21
+
22
+ class Numeric # :nodoc:
23
+ def to_liquid
24
+ self
25
+ end
26
+ end
27
+
28
+ class Time # :nodoc:
29
+ def to_liquid
30
+ self
31
+ end
32
+ end
33
+
34
+ class DateTime < Date # :nodoc:
35
+ def to_liquid
36
+ self
37
+ end
38
+ end
39
+
40
+ class Date # :nodoc:
41
+ def to_liquid
42
+ self
43
+ end
44
+ end
45
+
46
+ def true.to_liquid # :nodoc:
47
+ self
48
+ end
49
+
50
+ def false.to_liquid # :nodoc:
51
+ self
52
+ end
53
+
54
+ def nil.to_liquid # :nodoc:
55
+ self
56
+ 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,74 @@
1
+ module Liquid
2
+ class TableRow < Block
3
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
4
+
5
+ def initialize(tag_name, markup, tokens)
6
+ if markup =~ Syntax
7
+ @variable_name = $1
8
+ @collection_name = $2
9
+ @attributes = {}
10
+ markup.scan(TagAttributes) do |key, value|
11
+ @attributes[key] = value
12
+ end
13
+ else
14
+ raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
15
+ end
16
+
17
+ super
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
+ 'col' => col + 1,
46
+ 'col0' => col,
47
+ 'index0' => index,
48
+ 'rindex' => length - index,
49
+ 'rindex0' => length - index -1,
50
+ 'first' => (index == 0),
51
+ 'last' => (index == length - 1),
52
+ 'col_first' => (col == 0),
53
+ 'col_last' => (col == cols - 1)
54
+ }
55
+
56
+
57
+ col += 1
58
+
59
+ result << ["<td class=\"col#{col}\">"] + render_all(@nodelist, context) + ['</td>']
60
+
61
+ if col == cols and not (index == length - 1)
62
+ col = 0
63
+ row += 1
64
+ result << ["</tr>\n<tr class=\"row#{row}\">"]
65
+ end
66
+
67
+ end
68
+ end
69
+ result + ["</tr>\n"]
70
+ end
71
+ end
72
+
73
+ Template.register_tag('tablerow', TableRow)
74
+ end