locomotive_liquid 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,254 @@
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, :errors, :registers, :environments
17
+
18
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
19
+ @environments = [environments].flatten
20
+ @scopes = [(outer_scope || {})]
21
+ @registers = registers
22
+ @errors = []
23
+ @rethrow_errors = rethrow_errors
24
+ squash_instance_assigns_with_environments
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(filters)
35
+ filters = [filters].flatten.compact
36
+
37
+ filters.each do |f|
38
+ raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
39
+ strainer.extend(f)
40
+ end
41
+ end
42
+
43
+ def handle_error(e)
44
+ errors.push(e)
45
+ raise if @rethrow_errors
46
+
47
+ case e
48
+ when SyntaxError
49
+ "Liquid syntax error: #{e.message}"
50
+ else
51
+ "Liquid error: #{e.message}"
52
+ end
53
+ end
54
+
55
+
56
+ def invoke(method, *args)
57
+ if strainer.respond_to?(method)
58
+ strainer.__send__(method, *args)
59
+ else
60
+ args.first
61
+ end
62
+ end
63
+
64
+ # push new local scope on the stack. use <tt>Context#stack</tt> instead
65
+ def push(new_scope={})
66
+ raise StackLevelError, "Nesting too deep" if @scopes.length > 100
67
+ @scopes.unshift(new_scope)
68
+ end
69
+
70
+ # merge a hash of variables in the current local scope
71
+ def merge(new_scopes)
72
+ @scopes[0].merge!(new_scopes)
73
+ end
74
+
75
+ # pop from the stack. use <tt>Context#stack</tt> instead
76
+ def pop
77
+ raise ContextError if @scopes.size == 1
78
+ @scopes.shift
79
+ end
80
+
81
+ # pushes a new local scope on the stack, pops it at the end of the block
82
+ #
83
+ # Example:
84
+ #
85
+ # context.stack do
86
+ # context['var'] = 'hi'
87
+ # end
88
+ # context['var] #=> nil
89
+ #
90
+ def stack(new_scope={},&block)
91
+ result = nil
92
+ push(new_scope)
93
+ begin
94
+ result = yield
95
+ ensure
96
+ pop
97
+ end
98
+ result
99
+ end
100
+
101
+ def clear_instance_assigns
102
+ @scopes[0] = {}
103
+ end
104
+
105
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
106
+ def []=(key, value)
107
+ @scopes[0][key] = value
108
+ end
109
+
110
+ def [](key)
111
+ resolve(key)
112
+ end
113
+
114
+ def has_key?(key)
115
+ resolve(key) != nil
116
+ end
117
+
118
+ private
119
+
120
+ # Look up variable, either resolve directly after considering the name. We can directly handle
121
+ # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
122
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
123
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
124
+ #
125
+ # Example:
126
+ #
127
+ # products == empty #=> products.empty?
128
+ #
129
+ def resolve(key)
130
+ case key
131
+ when nil, 'nil', 'null', ''
132
+ nil
133
+ when 'true'
134
+ true
135
+ when 'false'
136
+ false
137
+ when 'blank'
138
+ :blank?
139
+ when 'empty'
140
+ :empty?
141
+ # Single quoted strings
142
+ when /^'(.*)'$/
143
+ $1.to_s
144
+ # Double quoted strings
145
+ when /^"(.*)"$/
146
+ $1.to_s
147
+ # Integer and floats
148
+ when /^(\d+)$/
149
+ $1.to_i
150
+ # Ranges
151
+ when /^\((\S+)\.\.(\S+)\)$/
152
+ (resolve($1).to_i..resolve($2).to_i)
153
+ # Floats
154
+ when /^(\d[\d\.]+)$/
155
+ $1.to_f
156
+ else
157
+ variable(key)
158
+ end
159
+ end
160
+
161
+ # fetches an object starting at the local scope and then moving up
162
+ # the hierachy
163
+ def find_variable(key)
164
+ scope = @scopes.find { |s| s.has_key?(key) }
165
+ if scope.nil?
166
+ @environments.each do |e|
167
+ if variable = lookup_and_evaluate(e, key)
168
+ scope = e
169
+ break
170
+ end
171
+ end
172
+ end
173
+ scope ||= @environments.last || @scopes.last
174
+ variable ||= lookup_and_evaluate(scope, key)
175
+
176
+ variable = variable.to_liquid
177
+ variable.context = self if variable.respond_to?(:context=)
178
+ return variable
179
+ end
180
+
181
+ # resolves namespaced queries gracefully.
182
+ #
183
+ # Example
184
+ #
185
+ # @context['hash'] = {"name" => 'tobi'}
186
+ # assert_equal 'tobi', @context['hash.name']
187
+ # assert_equal 'tobi', @context['hash["name"]']
188
+ #
189
+ def variable(markup)
190
+ parts = markup.scan(VariableParser)
191
+ square_bracketed = /^\[(.*)\]$/
192
+
193
+ first_part = parts.shift
194
+ if first_part =~ square_bracketed
195
+ first_part = resolve($1)
196
+ end
197
+
198
+ if object = find_variable(first_part)
199
+
200
+ parts.each do |part|
201
+ part = resolve($1) if part_resolved = (part =~ square_bracketed)
202
+
203
+ # If object is a hash- or array-like object we look for the
204
+ # presence of the key and if its available we return it
205
+ if object.respond_to?(:[]) and
206
+ ((object.respond_to?(:has_key?) and object.has_key?(part)) or
207
+ (object.respond_to?(:fetch) and part.is_a?(Integer)))
208
+
209
+ # if its a proc we will replace the entry with the proc
210
+ res = lookup_and_evaluate(object, part)
211
+ object = res.to_liquid
212
+
213
+ # Some special cases. If the part wasn't in square brackets and
214
+ # no key with the same name was found we interpret following calls
215
+ # as commands and call them on the current object
216
+ elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
217
+
218
+ object = object.send(part.intern).to_liquid
219
+
220
+ # No key was present with the desired value and it wasn't one of the directly supported
221
+ # keywords either. The only thing we got left is to return nil
222
+ else
223
+ return nil
224
+ end
225
+
226
+ # If we are dealing with a drop here we have to
227
+ object.context = self if object.respond_to?(:context=)
228
+ end
229
+ end
230
+
231
+ object
232
+ end
233
+
234
+ def lookup_and_evaluate(obj, key)
235
+ if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
236
+ obj[key] = value.call(self)
237
+ else
238
+ value
239
+ end
240
+ end
241
+
242
+ def squash_instance_assigns_with_environments
243
+ @scopes.last.each_key do |k|
244
+ @environments.each do |env|
245
+ if env.has_key?(k)
246
+ scopes.last[k] = lookup_and_evaluate(env, k)
247
+ break
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ end
254
+ end
@@ -0,0 +1,18 @@
1
+ module Liquid
2
+ class Document < Block
3
+ # we don't need markup to open this block
4
+ def initialize(tokens, context)
5
+ @context = context
6
+ parse(tokens)
7
+ end
8
+
9
+ # There isn't a real delimter
10
+ def block_delimiter
11
+ []
12
+ end
13
+
14
+ # Document blocks don't need to be terminated since they are not actually opened
15
+ def assert_missing_delimitation!
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
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
+ # for backward compatibility with Ruby 1.8
33
+ methods = self.class.public_instance_methods.map { |m| m.to_s }
34
+ if methods.include?(method.to_s)
35
+ send(method.to_sym)
36
+ else
37
+ before_method(method)
38
+ end
39
+ end
40
+
41
+ def has_key?(name)
42
+ true
43
+ end
44
+
45
+ def to_liquid
46
+ self
47
+ end
48
+
49
+ alias :[] :invoke_drop
50
+ end
51
+ 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