locomotivecms-liquid 2.6.0 → 4.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +62 -5
  3. data/README.md +4 -4
  4. data/lib/liquid.rb +16 -12
  5. data/lib/liquid/block.rb +37 -118
  6. data/lib/liquid/block_body.rb +131 -0
  7. data/lib/liquid/condition.rb +28 -17
  8. data/lib/liquid/context.rb +94 -146
  9. data/lib/liquid/document.rb +16 -10
  10. data/lib/liquid/drop.rb +8 -5
  11. data/lib/liquid/drops/inherited_block_drop.rb +24 -0
  12. data/lib/liquid/errors.rb +44 -5
  13. data/lib/liquid/expression.rb +33 -0
  14. data/lib/liquid/file_system.rb +17 -6
  15. data/lib/liquid/i18n.rb +2 -2
  16. data/lib/liquid/interrupts.rb +1 -1
  17. data/lib/liquid/lexer.rb +11 -9
  18. data/lib/liquid/locales/en.yml +2 -4
  19. data/lib/liquid/parser.rb +2 -1
  20. data/lib/liquid/parser_switching.rb +31 -0
  21. data/lib/liquid/profiler.rb +162 -0
  22. data/lib/liquid/profiler/hooks.rb +23 -0
  23. data/lib/liquid/range_lookup.rb +22 -0
  24. data/lib/liquid/resource_limits.rb +23 -0
  25. data/lib/liquid/standardfilters.rb +142 -67
  26. data/lib/liquid/strainer.rb +14 -4
  27. data/lib/liquid/tag.rb +22 -41
  28. data/lib/liquid/tags/assign.rb +15 -10
  29. data/lib/liquid/tags/break.rb +1 -1
  30. data/lib/liquid/tags/capture.rb +7 -9
  31. data/lib/liquid/tags/case.rb +28 -19
  32. data/lib/liquid/tags/comment.rb +2 -2
  33. data/lib/liquid/tags/continue.rb +1 -4
  34. data/lib/liquid/tags/cycle.rb +10 -14
  35. data/lib/liquid/tags/decrement.rb +3 -4
  36. data/lib/liquid/tags/extends.rb +28 -44
  37. data/lib/liquid/tags/for.rb +64 -42
  38. data/lib/liquid/tags/if.rb +30 -19
  39. data/lib/liquid/tags/ifchanged.rb +4 -4
  40. data/lib/liquid/tags/include.rb +30 -20
  41. data/lib/liquid/tags/increment.rb +3 -8
  42. data/lib/liquid/tags/inherited_block.rb +54 -56
  43. data/lib/liquid/tags/raw.rb +18 -10
  44. data/lib/liquid/tags/table_row.rb +72 -0
  45. data/lib/liquid/tags/unless.rb +5 -7
  46. data/lib/liquid/template.rb +113 -53
  47. data/lib/liquid/token.rb +18 -0
  48. data/lib/liquid/utils.rb +13 -4
  49. data/lib/liquid/variable.rb +68 -50
  50. data/lib/liquid/variable_lookup.rb +78 -0
  51. data/lib/liquid/version.rb +1 -1
  52. data/test/fixtures/en_locale.yml +9 -0
  53. data/test/integration/assign_test.rb +48 -0
  54. data/test/integration/blank_test.rb +106 -0
  55. data/test/integration/capture_test.rb +50 -0
  56. data/test/integration/context_test.rb +32 -0
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +271 -0
  59. data/test/integration/error_handling_test.rb +207 -0
  60. data/test/integration/filter_test.rb +125 -0
  61. data/test/integration/hash_ordering_test.rb +23 -0
  62. data/test/integration/output_test.rb +116 -0
  63. data/test/integration/parsing_quirks_test.rb +119 -0
  64. data/test/integration/render_profiling_test.rb +154 -0
  65. data/test/integration/security_test.rb +64 -0
  66. data/test/integration/standard_filter_test.rb +379 -0
  67. data/test/integration/tags/break_tag_test.rb +16 -0
  68. data/test/integration/tags/continue_tag_test.rb +16 -0
  69. data/test/integration/tags/extends_tag_test.rb +104 -0
  70. data/test/integration/tags/for_tag_test.rb +375 -0
  71. data/test/integration/tags/if_else_tag_test.rb +169 -0
  72. data/test/integration/tags/include_tag_test.rb +234 -0
  73. data/test/integration/tags/increment_tag_test.rb +24 -0
  74. data/test/integration/tags/raw_tag_test.rb +25 -0
  75. data/test/integration/tags/standard_tag_test.rb +297 -0
  76. data/test/integration/tags/statements_test.rb +113 -0
  77. data/test/integration/tags/table_row_test.rb +63 -0
  78. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  79. data/test/integration/template_test.rb +216 -0
  80. data/test/integration/variable_test.rb +82 -0
  81. data/test/test_helper.rb +83 -0
  82. data/test/unit/block_unit_test.rb +55 -0
  83. data/test/unit/condition_unit_test.rb +149 -0
  84. data/test/unit/context_unit_test.rb +482 -0
  85. data/test/unit/file_system_unit_test.rb +35 -0
  86. data/test/unit/i18n_unit_test.rb +37 -0
  87. data/test/unit/lexer_unit_test.rb +51 -0
  88. data/test/unit/module_ex_unit_test.rb +87 -0
  89. data/test/unit/parser_unit_test.rb +82 -0
  90. data/test/unit/regexp_unit_test.rb +44 -0
  91. data/test/unit/strainer_unit_test.rb +71 -0
  92. data/test/unit/tag_unit_test.rb +16 -0
  93. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  94. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  95. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  96. data/test/unit/template_unit_test.rb +70 -0
  97. data/test/unit/tokenizer_unit_test.rb +38 -0
  98. data/test/unit/variable_unit_test.rb +150 -0
  99. metadata +144 -15
  100. data/lib/extras/liquid_view.rb +0 -51
  101. data/lib/liquid/htmltags.rb +0 -74
  102. data/lib/liquid/tags/default_content.rb +0 -21
  103. data/lib/locomotivecms-liquid.rb +0 -1
@@ -3,19 +3,21 @@ module Liquid
3
3
  #
4
4
  # Example:
5
5
  #
6
- # c = Condition.new('1', '==', '1')
6
+ # c = Condition.new(1, '==', 1)
7
7
  # c.evaluate #=> true
8
8
  #
9
9
  class Condition #:nodoc:
10
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 && right ? left.include?(right) : false }
11
+ '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
+ '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
+ '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
+ '<'.freeze => :<,
15
+ '>'.freeze => :>,
16
+ '>='.freeze => :>=,
17
+ '<='.freeze => :<=,
18
+ 'contains'.freeze => lambda { |cond, left, right|
19
+ left && right && left.respond_to?(:include?) ? left.include?(right) : false
20
+ }
19
21
  }
20
22
 
21
23
  def self.operators
@@ -26,7 +28,9 @@ module Liquid
26
28
  attr_accessor :left, :operator, :right
27
29
 
28
30
  def initialize(left = nil, operator = nil, right = nil)
29
- @left, @operator, @right = left, operator, right
31
+ @left = left
32
+ @operator = operator
33
+ @right = right
30
34
  @child_relation = nil
31
35
  @child_condition = nil
32
36
  end
@@ -45,11 +49,13 @@ module Liquid
45
49
  end
46
50
 
47
51
  def or(condition)
48
- @child_relation, @child_condition = :or, condition
52
+ @child_relation = :or
53
+ @child_condition = condition
49
54
  end
50
55
 
51
56
  def and(condition)
52
- @child_relation, @child_condition = :and, condition
57
+ @child_relation = :and
58
+ @child_condition = condition
53
59
  end
54
60
 
55
61
  def attach(attachment)
@@ -61,7 +67,7 @@ module Liquid
61
67
  end
62
68
 
63
69
  def inspect
64
- "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
70
+ "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
65
71
  end
66
72
 
67
73
  private
@@ -90,16 +96,21 @@ module Liquid
90
96
  # If the operator is empty this means that the decision statement is just
91
97
  # a single variable. We can just poll this variable from the context and
92
98
  # return this as the result.
93
- return context[left] if op == nil
99
+ return context.evaluate(left) if op == nil
94
100
 
95
- left, right = context[left], context[right]
101
+ left = context.evaluate(left)
102
+ right = context.evaluate(right)
96
103
 
97
- operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
104
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
98
105
 
99
106
  if operation.respond_to?(:call)
100
107
  operation.call(self, left, right)
101
108
  elsif left.respond_to?(operation) and right.respond_to?(operation)
102
- left.send(operation, right)
109
+ begin
110
+ left.send(operation, right)
111
+ rescue ::ArgumentError => e
112
+ raise Liquid::ArgumentError.new(e.message)
113
+ end
103
114
  else
104
115
  nil
105
116
  end
@@ -14,27 +14,28 @@ module Liquid
14
14
  # context['bob'] #=> nil class Context
15
15
  class Context
16
16
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
-
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @rethrow_errors = rethrow_errors
24
- @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
17
+ attr_accessor :exception_handler
18
+
19
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
20
+ @environments = [environments].flatten
21
+ @scopes = [(outer_scope || {})]
22
+ @registers = registers
23
+ @errors = []
24
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
25
25
  squash_instance_assigns_with_environments
26
26
 
27
- @interrupts = []
28
- end
27
+ @this_stack_used = false
29
28
 
30
- def resource_limits_reached?
31
- (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
32
- (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
33
- (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
29
+ if rethrow_errors
30
+ self.exception_handler = ->(e) { true }
31
+ end
32
+
33
+ @interrupts = []
34
+ @filters = []
34
35
  end
35
36
 
36
37
  def strainer
37
- @strainer ||= Strainer.create(self)
38
+ @strainer ||= Strainer.create(self, @filters)
38
39
  end
39
40
 
40
41
  # Adds filters to this context.
@@ -43,17 +44,26 @@ module Liquid
43
44
  # for that
44
45
  def add_filters(filters)
45
46
  filters = [filters].flatten.compact
46
-
47
47
  filters.each do |f|
48
48
  raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
49
49
  Strainer.add_known_filter(f)
50
- strainer.extend(f)
50
+ end
51
+
52
+ # If strainer is already setup then there's no choice but to use a runtime
53
+ # extend call. If strainer is not yet created, we can utilize strainers
54
+ # cached class based API, which avoids busting the method cache.
55
+ if @strainer
56
+ filters.each do |f|
57
+ strainer.extend(f)
58
+ end
59
+ else
60
+ @filters.concat filters
51
61
  end
52
62
  end
53
63
 
54
64
  # are there any not handled interrupts?
55
65
  def has_interrupt?
56
- @interrupts.any?
66
+ !@interrupts.empty?
57
67
  end
58
68
 
59
69
  # push an interrupt to the stack. this interrupt is considered not handled.
@@ -66,26 +76,25 @@ module Liquid
66
76
  @interrupts.pop
67
77
  end
68
78
 
69
- def handle_error(e)
70
- errors.push(e)
71
- raise if @rethrow_errors
72
79
 
73
- case e
74
- when SyntaxError
75
- "Liquid syntax error: #{e.message}"
76
- else
77
- "Liquid error: #{e.message}"
80
+ def handle_error(e, token=nil)
81
+ if e.is_a?(Liquid::Error)
82
+ e.set_line_number_from_token(token)
78
83
  end
84
+
85
+ errors.push(e)
86
+ raise if exception_handler && exception_handler.call(e)
87
+ Liquid::Error.render(e)
79
88
  end
80
89
 
81
90
  def invoke(method, *args)
82
- strainer.invoke(method, *args)
91
+ strainer.invoke(method, *args).to_liquid
83
92
  end
84
93
 
85
94
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
86
95
  def push(new_scope={})
87
96
  @scopes.unshift(new_scope)
88
- raise StackLevelError, "Nesting too deep" if @scopes.length > 100
97
+ raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
89
98
  end
90
99
 
91
100
  # Merge a hash of variables in the current local scope
@@ -107,11 +116,19 @@ module Liquid
107
116
  # end
108
117
  #
109
118
  # context['var] #=> nil
110
- def stack(new_scope={})
111
- push(new_scope)
119
+ def stack(new_scope=nil)
120
+ old_stack_used = @this_stack_used
121
+ if new_scope
122
+ push(new_scope)
123
+ @this_stack_used = true
124
+ else
125
+ @this_stack_used = false
126
+ end
127
+
112
128
  yield
113
129
  ensure
114
- pop
130
+ pop if @this_stack_used
131
+ @this_stack_used = old_stack_used
115
132
  end
116
133
 
117
134
  def clear_instance_assigns
@@ -120,139 +137,71 @@ module Liquid
120
137
 
121
138
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
122
139
  def []=(key, value)
140
+ unless @this_stack_used
141
+ @this_stack_used = true
142
+ push({})
143
+ end
123
144
  @scopes[0][key] = value
124
145
  end
125
146
 
126
- def [](key)
127
- resolve(key)
147
+ # Look up variable, either resolve directly after considering the name. We can directly handle
148
+ # Strings, digits, floats and booleans (true,false).
149
+ # If no match is made we lookup the variable in the current scope and
150
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
151
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
152
+ #
153
+ # Example:
154
+ # products == empty #=> products.empty?
155
+ def [](expression)
156
+ evaluate(Expression.parse(expression))
128
157
  end
129
158
 
130
159
  def has_key?(key)
131
- resolve(key) != nil
160
+ self[key] != nil
132
161
  end
133
162
 
134
- private
163
+ def evaluate(object)
164
+ object.respond_to?(:evaluate) ? object.evaluate(self) : object
165
+ end
135
166
 
136
- LITERALS = {
137
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
138
- 'true' => true,
139
- 'false' => false,
140
- 'blank' => :blank?,
141
- 'empty' => :empty?
142
- }
143
-
144
- # Look up variable, either resolve directly after considering the name. We can directly handle
145
- # Strings, digits, floats and booleans (true,false).
146
- # If no match is made we lookup the variable in the current scope and
147
- # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
148
- # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
149
- #
150
- # Example:
151
- # products == empty #=> products.empty?
152
- def resolve(key)
153
- if LITERALS.key?(key)
154
- LITERALS[key]
155
- else
156
- case key
157
- when /^'(.*)'$/ # Single quoted strings
158
- $1
159
- when /^"(.*)"$/ # Double quoted strings
160
- $1
161
- when /^(-?\d+)$/ # Integer and floats
162
- $1.to_i
163
- when /^\((\S+)\.\.(\S+)\)$/ # Ranges
164
- (resolve($1).to_i..resolve($2).to_i)
165
- when /^(-?\d[\d\.]+)$/ # Floats
166
- $1.to_f
167
- else
168
- variable(key)
169
- end
170
- end
171
- end
167
+ # Fetches an object starting at the local scope and then moving up the hierachy
168
+ def find_variable(key)
172
169
 
173
- # Fetches an object starting at the local scope and then moving up the hierachy
174
- def find_variable(key)
175
- scope = @scopes.find { |s| s.has_key?(key) }
176
- variable = nil
170
+ # This was changed from find() to find_index() because this is a very hot
171
+ # path and find_index() is optimized in MRI to reduce object allocation
172
+ index = @scopes.find_index { |s| s.has_key?(key) }
173
+ scope = @scopes[index] if index
177
174
 
178
- if scope.nil?
179
- @environments.each do |e|
180
- if variable = lookup_and_evaluate(e, key)
181
- scope = e
182
- break
183
- end
175
+ variable = nil
176
+
177
+ if scope.nil?
178
+ @environments.each do |e|
179
+ variable = lookup_and_evaluate(e, key)
180
+ unless variable.nil?
181
+ scope = e
182
+ break
184
183
  end
185
184
  end
186
-
187
- scope ||= @environments.last || @scopes.last
188
- variable ||= lookup_and_evaluate(scope, key)
189
-
190
- variable = variable.to_liquid
191
- variable.context = self if variable.respond_to?(:context=)
192
-
193
- return variable
194
185
  end
195
186
 
196
- # Resolves namespaced queries gracefully.
197
- #
198
- # Example
199
- # @context['hash'] = {"name" => 'tobi'}
200
- # assert_equal 'tobi', @context['hash.name']
201
- # assert_equal 'tobi', @context['hash["name"]']
202
- def variable(markup)
203
- parts = markup.scan(VariableParser)
204
- square_bracketed = /^\[(.*)\]$/
205
-
206
- first_part = parts.shift
207
-
208
- if first_part =~ square_bracketed
209
- first_part = resolve($1)
210
- end
211
-
212
- if object = find_variable(first_part)
213
-
214
- parts.each do |part|
215
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
216
-
217
- # If object is a hash- or array-like object we look for the
218
- # presence of the key and if its available we return it
219
- if object.respond_to?(:[]) and
220
- ((object.respond_to?(:has_key?) and object.has_key?(part)) or
221
- (object.respond_to?(:fetch) and part.is_a?(Integer)))
222
-
223
- # if its a proc we will replace the entry with the proc
224
- res = lookup_and_evaluate(object, part)
225
- object = res.to_liquid
187
+ scope ||= @environments.last || @scopes.last
188
+ variable ||= lookup_and_evaluate(scope, key)
226
189
 
227
- # Some special cases. If the part wasn't in square brackets and
228
- # no key with the same name was found we interpret following calls
229
- # as commands and call them on the current object
230
- elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
190
+ variable = variable.to_liquid
191
+ variable.context = self if variable.respond_to?(:context=)
231
192
 
232
- object = object.send(part.intern).to_liquid
233
-
234
- # No key was present with the desired value and it wasn't one of the directly supported
235
- # keywords either. The only thing we got left is to return nil
236
- else
237
- return nil
238
- end
239
-
240
- # If we are dealing with a drop here we have to
241
- object.context = self if object.respond_to?(:context=)
242
- end
243
- end
244
-
245
- object
246
- end # variable
193
+ return variable
194
+ end
247
195
 
248
- def lookup_and_evaluate(obj, key)
249
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
250
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
251
- else
252
- value
253
- end
254
- end # lookup_and_evaluate
196
+ def lookup_and_evaluate(obj, key)
197
+ if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
198
+ obj[key] = (value.arity == 0) ? value.call : value.call(self)
199
+ else
200
+ value
201
+ end
202
+ end
255
203
 
204
+ private
256
205
  def squash_instance_assigns_with_environments
257
206
  @scopes.last.each_key do |k|
258
207
  @environments.each do |env|
@@ -264,5 +213,4 @@ module Liquid
264
213
  end
265
214
  end # squash_instance_assigns_with_environments
266
215
  end # Context
267
-
268
216
  end # Liquid
@@ -1,18 +1,24 @@
1
1
  module Liquid
2
- class Document < Block
3
- # we don't need markup to open this block
4
- def initialize(tokens, options = {})
5
- @options = options
6
- parse(tokens)
2
+ class Document < BlockBody
3
+ def self.parse(tokens, options)
4
+ doc = new
5
+ doc.parse(tokens, options)
6
+ doc
7
7
  end
8
8
 
9
- # There isn't a real delimiter
10
- def block_delimiter
11
- []
9
+ def parse(tokens, options)
10
+ super do |end_tag_name, end_tag_params|
11
+ unknown_tag(end_tag_name, options) if end_tag_name
12
+ end
12
13
  end
13
14
 
14
- # Document blocks don't need to be terminated since they are not actually opened
15
- def assert_missing_delimitation!
15
+ def unknown_tag(tag, options)
16
+ case tag
17
+ when 'else'.freeze, 'end'.freeze
18
+ raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_outer_tag".freeze, :tag => tag))
19
+ else
20
+ raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
21
+ end
16
22
  end
17
23
  end
18
24
  end
data/lib/liquid/drop.rb CHANGED
@@ -52,6 +52,10 @@ module Liquid
52
52
  self
53
53
  end
54
54
 
55
+ def to_s
56
+ self.class.name
57
+ end
58
+
55
59
  alias :[] :invoke_drop
56
60
 
57
61
  private
@@ -59,13 +63,12 @@ module Liquid
59
63
  # Check for method existence without invoking respond_to?, which creates symbols
60
64
  def self.invokable?(method_name)
61
65
  unless @invokable_methods
62
- # Ruby 1.8 compatibility: call to_s on method names (which are strings in 1.8, but already symbols in 1.9)
63
- blacklist = (Liquid::Drop.public_instance_methods + [:each]).map(&:to_s)
66
+ blacklist = Liquid::Drop.public_instance_methods + [:each]
64
67
  if include?(Enumerable)
65
- blacklist += Enumerable.public_instance_methods.map(&:to_s)
66
- blacklist -= [:sort, :count, :first, :min, :max, :include?].map(&:to_s)
68
+ blacklist += Enumerable.public_instance_methods
69
+ blacklist -= [:sort, :count, :first, :min, :max, :include?]
67
70
  end
68
- whitelist = [:to_liquid] + (public_instance_methods.map(&:to_s) - blacklist.map(&:to_s))
71
+ whitelist = [:to_liquid] + (public_instance_methods - blacklist)
69
72
  @invokable_methods = Set.new(whitelist.map(&:to_s))
70
73
  end
71
74
  @invokable_methods.include?(method_name.to_s)