agilitic-liquid 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,120 @@
1
+ module Liquid
2
+ # Container for liquid nodes which conveniently wraps decision making logic
3
+ #
4
+ # Example:
5
+ #
6
+ # c = Condition.new('1', '==', '1')
7
+ # c.evaluate #=> true
8
+ #
9
+ class Condition #:nodoc:
10
+ @@operators = {
11
+ '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
+ '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
+ '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
+ '<' => :<,
15
+ '>' => :>,
16
+ '>=' => :>=,
17
+ '<=' => :<=,
18
+ 'contains' => lambda { |cond, left, right| left.include?(right) },
19
+ }
20
+
21
+ def self.operators
22
+ @@operators
23
+ end
24
+
25
+ attr_reader :attachment
26
+ attr_accessor :left, :operator, :right
27
+
28
+ def initialize(left = nil, operator = nil, right = nil)
29
+ @left, @operator, @right = left, operator, right
30
+ @child_relation = nil
31
+ @child_condition = nil
32
+ end
33
+
34
+ def evaluate(context = Context.new)
35
+ result = interpret_condition(left, right, operator, context)
36
+
37
+ case @child_relation
38
+ when :or
39
+ result || @child_condition.evaluate(context)
40
+ when :and
41
+ result && @child_condition.evaluate(context)
42
+ else
43
+ result
44
+ end
45
+ end
46
+
47
+ def or(condition)
48
+ @child_relation, @child_condition = :or, condition
49
+ end
50
+
51
+ def and(condition)
52
+ @child_relation, @child_condition = :and, condition
53
+ end
54
+
55
+ def attach(attachment)
56
+ @attachment = attachment
57
+ end
58
+
59
+ def else?
60
+ false
61
+ end
62
+
63
+ def inspect
64
+ "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
65
+ end
66
+
67
+ private
68
+
69
+ def equal_variables(left, right)
70
+ if left.is_a?(Symbol)
71
+ if right.respond_to?(left)
72
+ return right.send(left.to_s)
73
+ else
74
+ return nil
75
+ end
76
+ end
77
+
78
+ if right.is_a?(Symbol)
79
+ if left.respond_to?(right)
80
+ return left.send(right.to_s)
81
+ else
82
+ return nil
83
+ end
84
+ end
85
+
86
+ left == right
87
+ end
88
+
89
+ def interpret_condition(left, right, op, context)
90
+ # If the operator is empty this means that the decision statement is just
91
+ # a single variable. We can just poll this variable from the context and
92
+ # return this as the result.
93
+ return context[left] if op == nil
94
+
95
+ left, right = context[left], context[right]
96
+
97
+ operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
98
+
99
+ if operation.respond_to?(:call)
100
+ operation.call(self, left, right)
101
+ elsif left.respond_to?(operation) and right.respond_to?(operation)
102
+ left.send(operation, right)
103
+ else
104
+ nil
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ class ElseCondition < Condition
111
+ def else?
112
+ true
113
+ end
114
+
115
+ def evaluate(context)
116
+ true
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,221 @@
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
+ raise FilterNotFound, "Filter '#{method}' not found"
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
+ # Single quoted strings
137
+ when /^'(.*)'$/
138
+ $1.to_s
139
+ # Double quoted strings
140
+ when /^"(.*)"$/
141
+ $1.to_s
142
+ # Integer and floats
143
+ when /^(\d+)$/
144
+ $1.to_i
145
+ # Ranges
146
+ when /^\((\S+)\.\.(\S+)\)$/
147
+ (resolve($1).to_i..resolve($2).to_i)
148
+ # Floats
149
+ when /^(\d[\d\.]+)$/
150
+ $1.to_f
151
+ else
152
+ variable(key)
153
+ end
154
+ end
155
+
156
+ # fetches an object starting at the local scope and then moving up
157
+ # the hierachy
158
+ def find_variable(key)
159
+ scope = @scopes[0..-2].find { |s| s.has_key?(key) } || @scopes.last
160
+ variable = scope[key]
161
+ variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
162
+ variable = variable.to_liquid
163
+ variable.context = self if variable.respond_to?(:context=)
164
+ return variable
165
+ end
166
+
167
+ # resolves namespaced queries gracefully.
168
+ #
169
+ # Example
170
+ #
171
+ # @context['hash'] = {"name" => 'tobi'}
172
+ # assert_equal 'tobi', @context['hash.name']
173
+ # assert_equal 'tobi', @context['hash["name"]']
174
+ #
175
+ def variable(markup)
176
+ parts = markup.scan(VariableParser)
177
+ square_bracketed = /^\[(.*)\]$/
178
+
179
+ first_part = parts.shift
180
+ if first_part =~ square_bracketed
181
+ first_part = resolve($1)
182
+ end
183
+
184
+ if object = find_variable(first_part)
185
+
186
+ parts.each do |part|
187
+ part = resolve($1) if part_resolved = (part =~ square_bracketed)
188
+
189
+ # If object is a hash- or array-like object we look for the
190
+ # presence of the key and if its available we return it
191
+ if object.respond_to?(:[]) and
192
+ ((object.respond_to?(:has_key?) and object.has_key?(part)) or
193
+ (object.respond_to?(:fetch) and part.is_a?(Integer)))
194
+
195
+ # if its a proc we will replace the entry with the proc
196
+ res = object[part]
197
+ res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
198
+ object = res.to_liquid
199
+
200
+ # Some special cases. If the part wasn't in square brackets and
201
+ # no key with the same name was found we interpret following calls
202
+ # as commands and call them on the current object
203
+ elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
204
+
205
+ object = object.send(part.intern).to_liquid
206
+
207
+ # No key was present with the desired value and it wasn't one of the directly supported
208
+ # keywords either. The only thing we got left is to return nil
209
+ else
210
+ return nil
211
+ end
212
+
213
+ # If we are dealing with a drop here we have to
214
+ object.context = self if object.respond_to?(:context=)
215
+ end
216
+ end
217
+
218
+ object
219
+ end
220
+ end
221
+ 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,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