liquid 3.0.6 → 4.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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