liquid 3.0.6 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +89 -58
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/lib/liquid.rb +7 -6
  5. data/lib/liquid/block.rb +31 -124
  6. data/lib/liquid/block_body.rb +54 -57
  7. data/lib/liquid/condition.rb +23 -22
  8. data/lib/liquid/context.rb +50 -42
  9. data/lib/liquid/document.rb +19 -9
  10. data/lib/liquid/drop.rb +12 -13
  11. data/lib/liquid/errors.rb +16 -17
  12. data/lib/liquid/expression.rb +15 -3
  13. data/lib/liquid/extensions.rb +7 -7
  14. data/lib/liquid/file_system.rb +3 -3
  15. data/lib/liquid/forloop_drop.rb +42 -0
  16. data/lib/liquid/i18n.rb +5 -5
  17. data/lib/liquid/interrupts.rb +1 -2
  18. data/lib/liquid/lexer.rb +6 -4
  19. data/lib/liquid/locales/en.yml +3 -1
  20. data/lib/liquid/parse_context.rb +37 -0
  21. data/lib/liquid/parser_switching.rb +4 -4
  22. data/lib/liquid/profiler.rb +18 -19
  23. data/lib/liquid/profiler/hooks.rb +7 -7
  24. data/lib/liquid/range_lookup.rb +16 -1
  25. data/lib/liquid/resource_limits.rb +23 -0
  26. data/lib/liquid/standardfilters.rb +101 -56
  27. data/lib/liquid/strainer.rb +4 -5
  28. data/lib/liquid/tablerowloop_drop.rb +62 -0
  29. data/lib/liquid/tag.rb +9 -8
  30. data/lib/liquid/tags/assign.rb +5 -4
  31. data/lib/liquid/tags/break.rb +0 -3
  32. data/lib/liquid/tags/capture.rb +1 -1
  33. data/lib/liquid/tags/case.rb +19 -12
  34. data/lib/liquid/tags/comment.rb +2 -2
  35. data/lib/liquid/tags/cycle.rb +6 -6
  36. data/lib/liquid/tags/decrement.rb +1 -4
  37. data/lib/liquid/tags/for.rb +93 -75
  38. data/lib/liquid/tags/if.rb +49 -44
  39. data/lib/liquid/tags/ifchanged.rb +0 -2
  40. data/lib/liquid/tags/include.rb +60 -52
  41. data/lib/liquid/tags/raw.rb +26 -4
  42. data/lib/liquid/tags/table_row.rb +12 -30
  43. data/lib/liquid/tags/unless.rb +3 -4
  44. data/lib/liquid/template.rb +23 -50
  45. data/lib/liquid/tokenizer.rb +31 -0
  46. data/lib/liquid/utils.rb +48 -8
  47. data/lib/liquid/variable.rb +46 -45
  48. data/lib/liquid/variable_lookup.rb +3 -3
  49. data/lib/liquid/version.rb +1 -1
  50. data/test/integration/assign_test.rb +8 -8
  51. data/test/integration/blank_test.rb +14 -14
  52. data/test/integration/context_test.rb +2 -2
  53. data/test/integration/document_test.rb +19 -0
  54. data/test/integration/drop_test.rb +42 -40
  55. data/test/integration/error_handling_test.rb +64 -45
  56. data/test/integration/filter_test.rb +60 -20
  57. data/test/integration/output_test.rb +26 -27
  58. data/test/integration/parsing_quirks_test.rb +15 -13
  59. data/test/integration/render_profiling_test.rb +20 -20
  60. data/test/integration/security_test.rb +5 -7
  61. data/test/integration/standard_filter_test.rb +119 -37
  62. data/test/integration/tags/break_tag_test.rb +1 -2
  63. data/test/integration/tags/continue_tag_test.rb +0 -1
  64. data/test/integration/tags/for_tag_test.rb +133 -98
  65. data/test/integration/tags/if_else_tag_test.rb +75 -77
  66. data/test/integration/tags/include_tag_test.rb +23 -30
  67. data/test/integration/tags/increment_tag_test.rb +10 -11
  68. data/test/integration/tags/raw_tag_test.rb +7 -1
  69. data/test/integration/tags/standard_tag_test.rb +121 -122
  70. data/test/integration/tags/statements_test.rb +3 -5
  71. data/test/integration/tags/table_row_test.rb +20 -19
  72. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  73. data/test/integration/template_test.rb +91 -45
  74. data/test/integration/variable_test.rb +23 -13
  75. data/test/test_helper.rb +33 -5
  76. data/test/unit/block_unit_test.rb +6 -5
  77. data/test/unit/condition_unit_test.rb +82 -77
  78. data/test/unit/context_unit_test.rb +48 -57
  79. data/test/unit/file_system_unit_test.rb +3 -3
  80. data/test/unit/i18n_unit_test.rb +2 -2
  81. data/test/unit/lexer_unit_test.rb +11 -8
  82. data/test/unit/parser_unit_test.rb +2 -2
  83. data/test/unit/regexp_unit_test.rb +1 -1
  84. data/test/unit/strainer_unit_test.rb +13 -2
  85. data/test/unit/tag_unit_test.rb +7 -2
  86. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  87. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  88. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  89. data/test/unit/template_unit_test.rb +6 -5
  90. data/test/unit/tokenizer_unit_test.rb +24 -7
  91. data/test/unit/variable_unit_test.rb +60 -43
  92. metadata +44 -41
  93. data/lib/liquid/module_ex.rb +0 -62
  94. data/lib/liquid/token.rb +0 -18
  95. data/test/unit/module_ex_unit_test.rb +0 -87
@@ -3,21 +3,26 @@ 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
- '=='.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) },
11
+ '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
12
+ '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
13
+ '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
14
14
  '<'.freeze => :<,
15
15
  '>'.freeze => :>,
16
16
  '>='.freeze => :>=,
17
17
  '<='.freeze => :<=,
18
- 'contains'.freeze => lambda { |cond, left, right|
19
- left && right && left.respond_to?(:include?) ? left.include?(right) : false
20
- }
18
+ 'contains'.freeze => lambda do |cond, left, right|
19
+ if left && right && left.respond_to?(:include?)
20
+ right = right.to_s if left.is_a?(String)
21
+ left.include?(right)
22
+ else
23
+ false
24
+ end
25
+ end
21
26
  }
22
27
 
23
28
  def self.operators
@@ -73,17 +78,17 @@ module Liquid
73
78
  private
74
79
 
75
80
  def equal_variables(left, right)
76
- if left.is_a?(Symbol)
77
- if right.respond_to?(left)
78
- return right.send(left.to_s)
81
+ if left.is_a?(Liquid::Expression::MethodLiteral)
82
+ if right.respond_to?(left.method_name)
83
+ return right.send(left.method_name)
79
84
  else
80
85
  return nil
81
86
  end
82
87
  end
83
88
 
84
- if right.is_a?(Symbol)
85
- if left.respond_to?(right)
86
- return left.send(right.to_s)
89
+ if right.is_a?(Liquid::Expression::MethodLiteral)
90
+ if left.respond_to?(right.method_name)
91
+ return left.send(right.method_name)
87
92
  else
88
93
  return nil
89
94
  end
@@ -96,36 +101,32 @@ module Liquid
96
101
  # If the operator is empty this means that the decision statement is just
97
102
  # a single variable. We can just poll this variable from the context and
98
103
  # return this as the result.
99
- return context[left] if op == nil
104
+ return context.evaluate(left) if op.nil?
100
105
 
101
- left = context[left]
102
- right = context[right]
106
+ left = context.evaluate(left)
107
+ right = context.evaluate(right)
103
108
 
104
109
  operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
105
110
 
106
111
  if operation.respond_to?(:call)
107
112
  operation.call(self, left, right)
108
- elsif left.respond_to?(operation) and right.respond_to?(operation)
113
+ elsif left.respond_to?(operation) && right.respond_to?(operation)
109
114
  begin
110
115
  left.send(operation, right)
111
116
  rescue ::ArgumentError => e
112
117
  raise Liquid::ArgumentError.new(e.message)
113
118
  end
114
- else
115
- nil
116
119
  end
117
120
  end
118
121
  end
119
122
 
120
-
121
123
  class ElseCondition < Condition
122
124
  def else?
123
125
  true
124
126
  end
125
127
 
126
- def evaluate(context)
128
+ def evaluate(_context)
127
129
  true
128
130
  end
129
131
  end
130
-
131
132
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Context keeps the variable stack and resolves variables, as well as keywords
4
3
  #
5
4
  # context['variable'] = 'testing'
@@ -14,41 +13,30 @@ module Liquid
14
13
  # context['bob'] #=> nil class Context
15
14
  class Context
16
15
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
- attr_accessor :exception_handler
16
+ attr_accessor :exception_handler, :template_name, :partial, :global_filter
18
17
 
19
18
  def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
20
19
  @environments = [environments].flatten
21
20
  @scopes = [(outer_scope || {})]
22
21
  @registers = registers
23
22
  @errors = []
24
- @resource_limits = resource_limits || Template.default_resource_limits.dup
25
- @resource_limits[:render_score_current] = 0
26
- @resource_limits[:assign_score_current] = 0
27
- @parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
23
+ @partial = false
24
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
28
25
  squash_instance_assigns_with_environments
29
26
 
30
27
  @this_stack_used = false
31
28
 
32
29
  if rethrow_errors
33
- self.exception_handler = ->(e) { true }
30
+ self.exception_handler = ->(e) { raise }
34
31
  end
35
32
 
36
33
  @interrupts = []
37
34
  @filters = []
35
+ @global_filter = nil
38
36
  end
39
37
 
40
- def increment_used_resources(key, obj)
41
- @resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
42
- obj.length
43
- else
44
- 1
45
- end
46
- end
47
-
48
- def resource_limits_reached?
49
- (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
50
- (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
51
- (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
38
+ def warnings
39
+ @warnings ||= []
52
40
  end
53
41
 
54
42
  def strainer
@@ -65,8 +53,12 @@ module Liquid
65
53
  @strainer = nil
66
54
  end
67
55
 
56
+ def apply_global_filter(obj)
57
+ global_filter.nil? ? obj : global_filter.call(obj)
58
+ end
59
+
68
60
  # are there any not handled interrupts?
69
- def has_interrupt?
61
+ def interrupt?
70
62
  !@interrupts.empty?
71
63
  end
72
64
 
@@ -80,15 +72,31 @@ module Liquid
80
72
  @interrupts.pop
81
73
  end
82
74
 
83
-
84
- def handle_error(e, token=nil)
75
+ def handle_error(e, line_number = nil)
85
76
  if e.is_a?(Liquid::Error)
86
- e.set_line_number_from_token(token)
77
+ e.template_name ||= template_name
78
+ e.line_number ||= line_number
87
79
  end
88
80
 
81
+ output = nil
82
+
83
+ if exception_handler
84
+ result = exception_handler.call(e)
85
+ case result
86
+ when Exception
87
+ e = result
88
+ if e.is_a?(Liquid::Error)
89
+ e.template_name ||= template_name
90
+ e.line_number ||= line_number
91
+ end
92
+ when String
93
+ output = result
94
+ else
95
+ raise if result
96
+ end
97
+ end
89
98
  errors.push(e)
90
- raise if exception_handler && exception_handler.call(e)
91
- Liquid::Error.render(e)
99
+ output || Liquid::Error.render(e)
92
100
  end
93
101
 
94
102
  def invoke(method, *args)
@@ -96,7 +104,7 @@ module Liquid
96
104
  end
97
105
 
98
106
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
99
- def push(new_scope={})
107
+ def push(new_scope = {})
100
108
  @scopes.unshift(new_scope)
101
109
  raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
102
110
  end
@@ -120,7 +128,7 @@ module Liquid
120
128
  # end
121
129
  #
122
130
  # context['var] #=> nil
123
- def stack(new_scope=nil)
131
+ def stack(new_scope = nil)
124
132
  old_stack_used = @this_stack_used
125
133
  if new_scope
126
134
  push(new_scope)
@@ -157,10 +165,10 @@ module Liquid
157
165
  # Example:
158
166
  # products == empty #=> products.empty?
159
167
  def [](expression)
160
- evaluate(@parsed_expression[expression])
168
+ evaluate(Expression.parse(expression))
161
169
  end
162
170
 
163
- def has_key?(key)
171
+ def key?(key)
164
172
  self[key] != nil
165
173
  end
166
174
 
@@ -170,10 +178,9 @@ module Liquid
170
178
 
171
179
  # Fetches an object starting at the local scope and then moving up the hierachy
172
180
  def find_variable(key)
173
-
174
181
  # This was changed from find() to find_index() because this is a very hot
175
182
  # path and find_index() is optimized in MRI to reduce object allocation
176
- index = @scopes.find_index { |s| s.has_key?(key) }
183
+ index = @scopes.find_index { |s| s.key?(key) }
177
184
  scope = @scopes[index] if index
178
185
 
179
186
  variable = nil
@@ -188,13 +195,13 @@ module Liquid
188
195
  end
189
196
  end
190
197
 
191
- scope ||= @environments.last || @scopes.last
192
- variable ||= lookup_and_evaluate(scope, key)
198
+ scope ||= @environments.last || @scopes.last
199
+ variable ||= lookup_and_evaluate(scope, key)
193
200
 
194
201
  variable = variable.to_liquid
195
202
  variable.context = self if variable.respond_to?(:context=)
196
203
 
197
- return variable
204
+ variable
198
205
  end
199
206
 
200
207
  def lookup_and_evaluate(obj, key)
@@ -206,15 +213,16 @@ module Liquid
206
213
  end
207
214
 
208
215
  private
209
- def squash_instance_assigns_with_environments
210
- @scopes.last.each_key do |k|
211
- @environments.each do |env|
212
- if env.has_key?(k)
213
- scopes.last[k] = lookup_and_evaluate(env, k)
214
- break
215
- end
216
+
217
+ def squash_instance_assigns_with_environments
218
+ @scopes.last.each_key do |k|
219
+ @environments.each do |env|
220
+ if env.key?(k)
221
+ scopes.last[k] = lookup_and_evaluate(env, k)
222
+ break
216
223
  end
217
224
  end
218
- end # squash_instance_assigns_with_environments
225
+ end
226
+ end # squash_instance_assigns_with_environments
219
227
  end # Context
220
228
  end # Liquid
@@ -1,17 +1,27 @@
1
1
  module Liquid
2
- class Document < Block
3
- def self.parse(tokens, options={})
4
- # we don't need markup to open this block
5
- super(nil, nil, tokens, options)
2
+ class Document < BlockBody
3
+ def self.parse(tokens, parse_context)
4
+ doc = new
5
+ doc.parse(tokens, parse_context)
6
+ doc
6
7
  end
7
8
 
8
- # There isn't a real delimiter
9
- def block_delimiter
10
- []
9
+ def parse(tokens, parse_context)
10
+ super do |end_tag_name, end_tag_params|
11
+ unknown_tag(end_tag_name, parse_context) if end_tag_name
12
+ end
13
+ rescue SyntaxError => e
14
+ e.line_number ||= parse_context.line_number
15
+ raise
11
16
  end
12
17
 
13
- # Document blocks don't need to be terminated since they are not actually opened
14
- def assert_missing_delimitation!
18
+ def unknown_tag(tag, parse_context)
19
+ case tag
20
+ when 'else'.freeze, 'end'.freeze
21
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
22
+ else
23
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -1,7 +1,6 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
5
  # Methods of drops are callable.
7
6
  # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,28 +18,26 @@ module Liquid
19
18
  # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
19
  # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
20
  #
22
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
- # catch all.
21
+ # Your drop can either implement the methods sans any parameters
22
+ # or implement the liquid_method_missing(name) method which is a catch all.
24
23
  class Drop
25
24
  attr_writer :context
26
25
 
27
- EMPTY_STRING = ''.freeze
28
-
29
26
  # Catch all for the method
30
- def before_method(method)
27
+ def liquid_method_missing(_method)
31
28
  nil
32
29
  end
33
30
 
34
31
  # called by liquid to invoke a drop
35
32
  def invoke_drop(method_or_key)
36
- if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
33
+ if self.class.invokable?(method_or_key)
37
34
  send(method_or_key)
38
35
  else
39
- before_method(method_or_key)
36
+ liquid_method_missing(method_or_key)
40
37
  end
41
38
  end
42
39
 
43
- def has_key?(name)
40
+ def key?(_name)
44
41
  true
45
42
  end
46
43
 
@@ -56,12 +53,14 @@ module Liquid
56
53
  self.class.name
57
54
  end
58
55
 
59
- alias :[] :invoke_drop
60
-
61
- private
56
+ alias_method :[], :invoke_drop
62
57
 
63
58
  # Check for method existence without invoking respond_to?, which creates symbols
64
59
  def self.invokable?(method_name)
60
+ invokable_methods.include?(method_name.to_s)
61
+ end
62
+
63
+ def self.invokable_methods
65
64
  unless @invokable_methods
66
65
  blacklist = Liquid::Drop.public_instance_methods + [:each]
67
66
  if include?(Enumerable)
@@ -71,7 +70,7 @@ module Liquid
71
70
  whitelist = [:to_liquid] + (public_instance_methods - blacklist)
72
71
  @invokable_methods = Set.new(whitelist.map(&:to_s))
73
72
  end
74
- @invokable_methods.include?(method_name.to_s)
73
+ @invokable_methods
75
74
  end
76
75
  end
77
76
  end
@@ -1,9 +1,10 @@
1
1
  module Liquid
2
2
  class Error < ::StandardError
3
3
  attr_accessor :line_number
4
+ attr_accessor :template_name
4
5
  attr_accessor :markup_context
5
6
 
6
- def to_s(with_prefix=true)
7
+ def to_s(with_prefix = true)
7
8
  str = ""
8
9
  str << message_prefix if with_prefix
9
10
  str << super()
@@ -16,17 +17,11 @@ module Liquid
16
17
  str
17
18
  end
18
19
 
19
- def set_line_number_from_token(token)
20
- return unless token.respond_to?(:line_number)
21
- return if self.line_number
22
- self.line_number = token.line_number
23
- end
24
-
25
20
  def self.render(e)
26
21
  if e.is_a?(Liquid::Error)
27
22
  e.to_s
28
23
  else
29
- "Liquid error: #{e.to_s}"
24
+ "Liquid error: #{e}"
30
25
  end
31
26
  end
32
27
 
@@ -41,7 +36,9 @@ module Liquid
41
36
  end
42
37
 
43
38
  if line_number
44
- str << " (line #{line_number})"
39
+ str << " ("
40
+ str << template_name << " " if template_name
41
+ str << "line " << line_number.to_s << ")"
45
42
  end
46
43
 
47
44
  str << ": "
@@ -49,12 +46,14 @@ module Liquid
49
46
  end
50
47
  end
51
48
 
52
- class ArgumentError < Error; end
53
- class ContextError < Error; end
54
- class FileSystemError < Error; end
55
- class StandardError < Error; end
56
- class SyntaxError < Error; end
57
- class StackLevelError < Error; end
58
- class TaintedError < Error; end
59
- class MemoryError < Error; end
49
+ ArgumentError = Class.new(Error)
50
+ ContextError = Class.new(Error)
51
+ FileSystemError = Class.new(Error)
52
+ StandardError = Class.new(Error)
53
+ SyntaxError = Class.new(Error)
54
+ StackLevelError = Class.new(Error)
55
+ TaintedError = Class.new(Error)
56
+ MemoryError = Class.new(Error)
57
+ ZeroDivisionError = Class.new(Error)
58
+ FloatDomainError = Class.new(Error)
60
59
  end