liquid 5.12.0 → 5.14.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ede4a234547978b59f059b4a2939bfb3c91092952c45ef8616f7874c2f9902e3
4
- data.tar.gz: 4f489720f234a848498a390a76ba89e97bfa6b5d8c708ac9b6de293748ce6dd3
3
+ metadata.gz: 160a429434128c743e92aa594ff0c0da4442ff24eaafec960b3d3bc877bd623d
4
+ data.tar.gz: 86b6682d426999c6f1528c3898671414f0f96ebf955498535e30171c639e380f
5
5
  SHA512:
6
- metadata.gz: c0417cca5aece94bb341d38ba6e20deac2437fa453e926c296b6725858a35e845f25c548d1ddb364c675223667284ee088cf1fe76c151ef905b70cda08b977d3
7
- data.tar.gz: ec169349a4b973bc11d5f52a2f4f20e7467eaecae1521cdec014b9e42f2de2cc955b1789f44e0249db95f9d8fc5d7659fed6897c699753f8f4111ce16c742b36
6
+ metadata.gz: 8f9fb1adebc5974b7f4e0eca38947335b359cb30fc6c6ae4058516573d760695c5ab8ef2cbf1f4cc2226d23d8a27126faf407b5da7ccc244a0b976491f0a3328
7
+ data.tar.gz: d7d200ba0a5b62d4e86bcf197f7f7a06ed2b448c25e73e196787164892b517b24d3fc4fa3f6296d03945f06755d8004590b8dc1bd0df13743ba4f8ba805e2939
data/History.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 5.14.0
4
+
5
+ * Avoid materializing integer ranges in `for` and `tablerow` loops, and account for each visited range item [Ian Ker-Seymer]
6
+
7
+ ## 5.13.0
8
+
9
+ * Add TruffleRuby in CI [Benoit Daloze]
10
+ * Skip slow test raising many exceptions on non-CRuby [Benoit Daloze]
11
+ * Reject bare-bracket syntax in strict2 and introduce `self` keyword by [Alok Swamy]
12
+ * Add strict2_parse to assign and capture tags by [Alok Swamy]
13
+ * Add strict2_parse to increment and decrement tags by [Alok Swamy]
14
+ * Update liquid-spec adapters for `missing_features` [Ian Ker-Seymer]
15
+ * Prevent `SelfDrop` context mutation across render boundaries [Guilherme Carreiro]
16
+ * Fix `SelfDrop` equality [Guilherme Carreiro]
17
+ * Let environment `self` shadow `SelfDrop` [Ian Ker-Seymer]
18
+
3
19
  ## 5.11.0
4
20
  * Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
5
21
  * Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
data/README.md CHANGED
@@ -149,6 +149,13 @@ template.render!({ 'x' => 1}, { strict_variables: true })
149
149
  #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
150
150
  ```
151
151
 
152
+ ### Resource limits
153
+
154
+ `render_score_limit` and `cumulative_render_score_limit` account for each item visited by
155
+ integer-range `for` and `tablerow` loops, including loops with empty bodies. This bounds range
156
+ iteration work when a score limit is configured; `render_length_limit` only bounds generated
157
+ output and does not by itself limit CPU work for output-free loops.
158
+
152
159
  ### Usage tracking
153
160
 
154
161
  To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
@@ -99,7 +99,9 @@ module Liquid
99
99
  context.handle_error(exc, line_number)
100
100
  else
101
101
  error_message = context.handle_error(exc, line_number)
102
- unless blank_tag # conditional for backwards compatibility
102
+ error_mode = context.registers.static[:template_error_mode]
103
+ suppress_error_text = blank_tag && error_mode != :strict2 && error_mode != :rigid
104
+ unless suppress_error_text # blank-tag suppression is kept for backwards compatibility outside strict2
103
105
  output << error_message
104
106
  end
105
107
  end
@@ -187,6 +187,15 @@ module Liquid
187
187
  find_variable(key, raise_on_not_found: false) != nil
188
188
  end
189
189
 
190
+ # Checks whether a variable is defined in any scope, including nil-valued keys.
191
+ # Unlike #key?, this uses Hash#key? so that variables explicitly set to nil
192
+ # are still considered defined.
193
+ def variable_defined?(key)
194
+ @scopes.any? { |s| s.key?(key) } ||
195
+ @environments.any? { |e| e.key?(key) } ||
196
+ @static_environments.any? { |e| e.key?(key) }
197
+ end
198
+
190
199
  def evaluate(object)
191
200
  object.respond_to?(:evaluate) ? object.evaluate(self) : object
192
201
  end
@@ -197,12 +206,21 @@ module Liquid
197
206
  # path and find_index() is optimized in MRI to reduce object allocation
198
207
  index = @scopes.find_index { |s| s.key?(key) }
199
208
 
209
+ fallback_to_self_drop = key == Expression::SELF && index.nil?
210
+
200
211
  variable = if index
201
212
  lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
202
213
  else
203
- try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
214
+ try_variable_find_in_environments(
215
+ key,
216
+ raise_on_not_found: raise_on_not_found && !fallback_to_self_drop,
217
+ )
204
218
  end
205
219
 
220
+ # `self` resolves to a SelfDrop (enabling `self['var']` lookups),
221
+ # but only after the normal environment lookup doesn't find a value.
222
+ return @self_drop ||= SelfDrop.new(self) if fallback_to_self_drop && variable.nil?
223
+
206
224
  # update variable's context before invoking #to_liquid
207
225
  variable.context = self if variable.respond_to?(:context=)
208
226
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Liquid
4
4
  class Expression
5
+ SELF = 'self'
6
+
5
7
  LITERALS = {
6
8
  nil => nil,
7
9
  'nil' => nil,
@@ -38,7 +38,7 @@ module Liquid
38
38
 
39
39
  def new_parser(input)
40
40
  @string_scanner.string = input
41
- Parser.new(@string_scanner)
41
+ Parser.new(@string_scanner, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
42
42
  end
43
43
 
44
44
  def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
data/lib/liquid/parser.rb CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Liquid
4
4
  class Parser
5
- def initialize(input)
5
+ def initialize(input, reject_bare_brackets: false)
6
6
  ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
7
7
  @tokens = Lexer.tokenize(ss)
8
8
  @p = 0 # pointer to current location
9
+ @reject_bare_brackets = reject_bare_brackets
9
10
  end
10
11
 
11
12
  def jump(point)
@@ -53,6 +54,9 @@ module Liquid
53
54
  str = consume
54
55
  str << variable_lookups
55
56
  when :open_square
57
+ if @reject_bare_brackets
58
+ raise SyntaxError, "Bare bracket access is not allowed. Use #{Expression::SELF}['...'] instead"
59
+ end
56
60
  str = consume.dup
57
61
  str << expression
58
62
  str << consume(:close_square)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class RangeSlice
5
+ attr_reader :length
6
+
7
+ def initialize(range, from, to, resource_limits)
8
+ range_length = range.end - range.begin
9
+ range_length += 1 unless range.exclude_end?
10
+ range_length = 0 if range_length.negative?
11
+
12
+ start = [from, 0].max
13
+ finish = [to || range_length, range_length].min
14
+
15
+ @first = range.begin + start
16
+ @length = [finish - start, 0].max
17
+ @direction = 1
18
+ @resource_limits = resource_limits
19
+ end
20
+
21
+ def empty?
22
+ @length.zero?
23
+ end
24
+
25
+ def each
26
+ return enum_for(:each) unless block_given?
27
+
28
+ value = @first
29
+ @length.times do
30
+ @resource_limits.increment_render_score(1)
31
+ yield value
32
+ value += @direction
33
+ end
34
+ end
35
+
36
+ def reverse!
37
+ unless empty?
38
+ @first += @direction * (@length - 1)
39
+ @direction = -@direction
40
+ end
41
+ self
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type object
6
+ # @liquid_name self
7
+ # @liquid_summary
8
+ # Provides access to variables through the current scope chain.
9
+ # @liquid_description
10
+ # The `self` object resolves variables through the normal lookup hierarchy
11
+ # (local > file > global) without exposing filters, interrupts, errors,
12
+ # or other context internals. It's used when bare bracket notation
13
+ # (`['variable']`) needs to be replaced with an explicit variable lookup.
14
+ #
15
+ # If `self` is explicitly assigned as a local variable (e.g. `{% assign self = 'value' %}`),
16
+ # then the local value takes precedence over the `self` object.
17
+ # @liquid_access global
18
+ class SelfDrop < Drop
19
+ def initialize(self_context)
20
+ super()
21
+ @self_context = self_context
22
+ end
23
+
24
+ def [](key)
25
+ @self_context.find_variable(key)
26
+ rescue UndefinedVariable
27
+ nil
28
+ end
29
+
30
+ def key?(key)
31
+ @self_context.variable_defined?(key)
32
+ end
33
+
34
+ def to_liquid
35
+ self
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(SelfDrop) && other.self_context.equal?(@self_context)
40
+ end
41
+
42
+ alias_method :eql?, :==
43
+
44
+ def hash
45
+ @self_context.object_id.hash
46
+ end
47
+
48
+ protected
49
+
50
+ attr_reader :self_context
51
+
52
+ undef context=
53
+ end
54
+ end
@@ -8,10 +8,19 @@ module Liquid
8
8
  MAX_I32 = (1 << 31) - 1
9
9
  private_constant :MAX_I32
10
10
 
11
- MIN_I64 = -(1 << 63)
12
- MAX_I64 = (1 << 63) - 1
13
- I64_RANGE = MIN_I64..MAX_I64
14
- private_constant :MIN_I64, :MAX_I64, :I64_RANGE
11
+ supports_64bit_indices = begin
12
+ [][1 << 33, 1 << 33]
13
+ true
14
+ rescue RangeError
15
+ false
16
+ end
17
+
18
+ INDEX_RANGE = if supports_64bit_indices
19
+ (-(1 << 63))..((1 << 63) - 1)
20
+ else
21
+ (-(1 << 31))..((1 << 31) - 1)
22
+ end
23
+ private_constant :INDEX_RANGE
15
24
 
16
25
  HTML_ESCAPE = {
17
26
  '&' => '&amp;',
@@ -214,11 +223,11 @@ module Liquid
214
223
  Utils.to_s(input).slice(offset, length) || ''
215
224
  end
216
225
  rescue RangeError
217
- if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
226
+ if INDEX_RANGE.cover?(length) && INDEX_RANGE.cover?(offset)
218
227
  raise # unexpected error
219
228
  end
220
- offset = offset.clamp(I64_RANGE)
221
- length = length.clamp(I64_RANGE)
229
+ offset = offset.clamp(INDEX_RANGE)
230
+ length = length.clamp(INDEX_RANGE)
222
231
  retry
223
232
  end
224
233
  end
@@ -18,6 +18,8 @@ module Liquid
18
18
  # @liquid_syntax_keyword variable_name The name of the variable being created.
19
19
  # @liquid_syntax_keyword value The value you want to assign to the variable.
20
20
  class Assign < Tag
21
+ include ParserSwitching
22
+
21
23
  Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
22
24
 
23
25
  # @api private
@@ -29,6 +31,10 @@ module Liquid
29
31
 
30
32
  def initialize(tag_name, markup, parse_context)
31
33
  super
34
+ parse_with_selected_parser(markup)
35
+ end
36
+
37
+ def lax_parse(markup)
32
38
  if markup =~ Syntax
33
39
  @to = Regexp.last_match(1)
34
40
  @from = Variable.new(Regexp.last_match(2), parse_context)
@@ -37,6 +43,25 @@ module Liquid
37
43
  end
38
44
  end
39
45
 
46
+ def strict_parse(markup)
47
+ lax_parse(markup)
48
+ end
49
+
50
+ def strict2_parse(markup)
51
+ unless markup =~ Syntax
52
+ self.class.raise_syntax_error(parse_context)
53
+ end
54
+
55
+ lhs = Regexp.last_match(1).strip
56
+ rhs = Regexp.last_match(2)
57
+
58
+ p = @parse_context.new_parser(lhs)
59
+ @to = p.consume(:id)
60
+ p.consume(:end_of_string)
61
+
62
+ @from = Variable.new(rhs, parse_context)
63
+ end
64
+
40
65
  def render_to_output_buffer(context, output)
41
66
  val = @from.render(context)
42
67
  context.scopes.last[@to] = val
@@ -20,10 +20,18 @@ module Liquid
20
20
  # @liquid_syntax_keyword variable The name of the variable being created.
21
21
  # @liquid_syntax_keyword value The value you want to assign to the variable.
22
22
  class Capture < Block
23
+ include ParserSwitching
24
+
23
25
  Syntax = /(#{VariableSignature}+)/o
24
26
 
27
+ attr_reader :to
28
+
25
29
  def initialize(tag_name, markup, options)
26
30
  super
31
+ parse_with_selected_parser(markup)
32
+ end
33
+
34
+ def lax_parse(markup)
27
35
  if markup =~ Syntax
28
36
  @to = Regexp.last_match(1)
29
37
  else
@@ -31,6 +39,16 @@ module Liquid
31
39
  end
32
40
  end
33
41
 
42
+ def strict_parse(markup)
43
+ lax_parse(markup)
44
+ end
45
+
46
+ def strict2_parse(markup)
47
+ p = @parse_context.new_parser(markup.strip)
48
+ @to = p.consume(:id)
49
+ p.consume(:end_of_string)
50
+ end
51
+
34
52
  def render_to_output_buffer(context, output)
35
53
  context.resource_limits.with_capture do
36
54
  capture_output = render(context)
@@ -23,13 +23,29 @@ module Liquid
23
23
  # {% decrement variable_name %}
24
24
  # @liquid_syntax_keyword variable_name The name of the variable being decremented.
25
25
  class Decrement < Tag
26
+ include ParserSwitching
27
+
26
28
  attr_reader :variable_name
27
29
 
28
30
  def initialize(tag_name, markup, options)
29
31
  super
32
+ parse_with_selected_parser(markup)
33
+ end
34
+
35
+ def lax_parse(markup)
30
36
  @variable_name = markup.strip
31
37
  end
32
38
 
39
+ def strict_parse(markup)
40
+ lax_parse(markup)
41
+ end
42
+
43
+ def strict2_parse(markup)
44
+ p = @parse_context.new_parser(markup.strip)
45
+ @variable_name = p.consume(:id)
46
+ p.consume(:end_of_string)
47
+ end
48
+
33
49
  def render_to_output_buffer(context, output)
34
50
  counter_environment = context.environments.first
35
51
  value = counter_environment[@variable_name] || 0
@@ -130,7 +130,6 @@ module Liquid
130
130
  end
131
131
 
132
132
  collection = context.evaluate(@collection_name)
133
- collection = collection.to_a if collection.is_a?(Range)
134
133
 
135
134
  limit_value = context.evaluate(@limit)
136
135
  to = if limit_value.nil?
@@ -139,7 +138,9 @@ module Liquid
139
138
  Utils.to_integer(limit_value) + from
140
139
  end
141
140
 
142
- segment = Utils.slice_collection(collection, from, to)
141
+ segment = Utils.slice_collection_for_iteration(
142
+ collection, from, to, context.resource_limits, use_range_to_a: true
143
+ )
143
144
  segment.reverse! if @reversed
144
145
 
145
146
  offsets[@name] = from + segment.length
@@ -20,7 +20,8 @@ module Liquid
20
20
  class Include < Tag
21
21
  prepend Tag::Disableable
22
22
 
23
- SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
23
+ FOR = 'for'
24
+ SYNTAX = /(#{QuotedFragment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
24
25
  Syntax = SYNTAX
25
26
 
26
27
  attr_reader :template_name_expr, :variable_name_expr, :attributes
@@ -84,12 +85,18 @@ module Liquid
84
85
  alias_method :parse_context, :options
85
86
  private :parse_context
86
87
 
88
+ def for_loop?
89
+ @is_for_loop
90
+ end
91
+
87
92
  def strict2_parse(markup)
88
93
  p = @parse_context.new_parser(markup)
89
94
 
90
95
  @template_name_expr = safe_parse_expression(p)
91
- @variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with")
96
+ with_or_for = p.id?("for") || p.id?("with")
97
+ @variable_name_expr = safe_parse_expression(p) if with_or_for
92
98
  @alias_name = p.consume(:id) if p.id?("as")
99
+ @is_for_loop = (with_or_for == FOR)
93
100
 
94
101
  p.consume?(:comma)
95
102
 
@@ -111,11 +118,13 @@ module Liquid
111
118
  def lax_parse(markup)
112
119
  if markup =~ SYNTAX
113
120
  template_name = Regexp.last_match(1)
114
- variable_name = Regexp.last_match(3)
121
+ with_or_for = Regexp.last_match(3)
122
+ variable_name = Regexp.last_match(4)
115
123
 
116
- @alias_name = Regexp.last_match(5)
124
+ @alias_name = Regexp.last_match(6)
117
125
  @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
118
126
  @template_name_expr = parse_expression(template_name)
127
+ @is_for_loop = (with_or_for == FOR)
119
128
  @attributes = {}
120
129
 
121
130
  markup.scan(TagAttributes) do |key, value|
@@ -23,13 +23,29 @@ module Liquid
23
23
  # {% increment variable_name %}
24
24
  # @liquid_syntax_keyword variable_name The name of the variable being incremented.
25
25
  class Increment < Tag
26
+ include ParserSwitching
27
+
26
28
  attr_reader :variable_name
27
29
 
28
30
  def initialize(tag_name, markup, options)
29
31
  super
32
+ parse_with_selected_parser(markup)
33
+ end
34
+
35
+ def lax_parse(markup)
30
36
  @variable_name = markup.strip
31
37
  end
32
38
 
39
+ def strict_parse(markup)
40
+ lax_parse(markup)
41
+ end
42
+
43
+ def strict2_parse(markup)
44
+ p = @parse_context.new_parser(markup.strip)
45
+ @variable_name = p.consume(:id)
46
+ p.consume(:end_of_string)
47
+ end
48
+
33
49
  def render_to_output_buffer(context, output)
34
50
  counter_environment = context.environments.first
35
51
  value = counter_environment[@variable_name] || 0
@@ -85,12 +85,13 @@ module Liquid
85
85
  from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
86
86
  to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
87
87
 
88
- collection = Utils.slice_collection(collection, from, to)
88
+ collection = Utils.slice_collection_for_iteration(collection, from, to, context.resource_limits, allow_endless: true)
89
89
  length = collection.length
90
90
 
91
91
  cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
92
92
 
93
93
  output << "<tr class=\"row1\">\n"
94
+ context.resource_limits.increment_write_score(output)
94
95
  context.stack do
95
96
  tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
96
97
  context['tablerowloop'] = tablerowloop
@@ -101,6 +102,7 @@ module Liquid
101
102
  output << "<td class=\"col#{tablerowloop.col}\">"
102
103
  super
103
104
  output << '</td>'
105
+ context.resource_limits.increment_write_score(output)
104
106
 
105
107
  # Handle any interrupts if they exist.
106
108
  if context.interrupt?
@@ -110,6 +112,7 @@ module Liquid
110
112
 
111
113
  if tablerowloop.col_last && !tablerowloop.last
112
114
  output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
115
+ context.resource_limits.increment_write_score(output)
113
116
  end
114
117
 
115
118
  tablerowloop.send(:increment!)
@@ -117,6 +120,7 @@ module Liquid
117
120
  end
118
121
 
119
122
  output << "</tr>\n"
123
+ context.resource_limits.increment_write_score(output)
120
124
  output
121
125
  end
122
126
 
@@ -151,8 +151,10 @@ module Liquid
151
151
 
152
152
  c
153
153
  when Liquid::Drop
154
- drop = args.shift
155
- drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
154
+ drop = args.shift
155
+ c = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
156
+ drop.context = c if drop.respond_to?(:context=)
157
+ c
156
158
  when Hash
157
159
  Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
158
160
  when nil
@@ -187,12 +189,20 @@ module Liquid
187
189
 
188
190
  context.template_name ||= name
189
191
 
192
+ previous_error_mode = context.registers.static[:template_error_mode]
193
+ context.registers.static[:template_error_mode] = @error_mode
194
+
190
195
  begin
191
196
  # render the nodelist.
192
197
  @root.render_to_output_buffer(context, output || +'')
193
198
  rescue Liquid::MemoryError => e
194
199
  context.handle_error(e)
195
200
  ensure
201
+ if previous_error_mode
202
+ context.registers.static[:template_error_mode] = previous_error_mode
203
+ else
204
+ context.registers.static.delete(:template_error_mode)
205
+ end
196
206
  @errors = context.errors
197
207
  end
198
208
  end
@@ -224,6 +234,7 @@ module Liquid
224
234
  end
225
235
 
226
236
  @warnings = parse_context.warnings
237
+ @error_mode = parse_context.error_mode
227
238
  parse_context
228
239
  end
229
240
 
data/lib/liquid/utils.rb CHANGED
@@ -13,6 +13,26 @@ module Liquid
13
13
  end
14
14
  end
15
15
 
16
+ # This is intentionally separate from slice_collection, whose Array-returning
17
+ # behavior is used outside of the iteration tags.
18
+ def self.slice_collection_for_iteration(
19
+ collection, from, to, resource_limits, allow_endless: false, use_range_to_a: false
20
+ )
21
+ if integer_range?(collection)
22
+ RangeSlice.new(collection, from, to, resource_limits)
23
+ elsif collection.is_a?(Range)
24
+ if use_range_to_a && range_method_overridden?(collection, :to_a)
25
+ # For historically honored custom Range#to_a. Charge the resulting
26
+ # selection before buffering it, just as for a custom #each.
27
+ slice_collection_for_iteration_using_each(collection.to_a, from, to, resource_limits)
28
+ else
29
+ slice_range_using_each(collection, from, to, resource_limits, allow_endless: allow_endless)
30
+ end
31
+ else
32
+ slice_collection(collection, from, to)
33
+ end
34
+ end
35
+
16
36
  def self.slice_collection_using_each(collection, from, to)
17
37
  segments = []
18
38
  index = 0
@@ -38,6 +58,55 @@ module Liquid
38
58
  segments
39
59
  end
40
60
 
61
+ # Arithmetic slicing must not bypass a Range subclass's custom #each.
62
+ def self.integer_range?(collection)
63
+ collection.instance_of?(Range) && collection.begin.is_a?(Integer) && collection.end.is_a?(Integer)
64
+ end
65
+ private_class_method :integer_range?
66
+
67
+ # Preserve support for Ruby-supplied string ranges and custom Range#each.
68
+ # Their selected length cannot be inferred from integer bounds, but the tags
69
+ # need it before rendering for loop metadata, continuation offsets, and columns.
70
+ # Buffer the selection so we do not have to replay a potentially custom iterator.
71
+ def self.slice_range_using_each(collection, from, to, resource_limits, allow_endless:)
72
+ # TableRow historically accepted an endless subclass when its custom #each
73
+ # was finite, while For historically raised through Range#to_a.
74
+ if collection.end.nil? && !(allow_endless && (!to.nil? || range_method_overridden?(collection, :each)))
75
+ raise RangeError, "cannot convert endless range to an array"
76
+ end
77
+ if collection.begin.nil? && !range_method_overridden?(collection, :each)
78
+ raise TypeError, "can't iterate from NilClass"
79
+ end
80
+
81
+ slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
82
+ end
83
+ private_class_method :slice_range_using_each
84
+
85
+ # Custom Range#each can make a nominally beginless range finite; standard
86
+ # beginless ranges were rejected before reaching this budgeted traversal.
87
+ def self.slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
88
+ return [] if to && to <= from
89
+
90
+ segments = []
91
+ index = 0
92
+ collection.each do |item|
93
+ break if to && to <= index
94
+
95
+ # Charge preparation, including skipped offsets, before buffering; checking
96
+ # only while rendering the buffered values would leave this work unbudgeted.
97
+ resource_limits.increment_render_score(1)
98
+ segments << item if from <= index
99
+ index += 1
100
+ end
101
+ segments
102
+ end
103
+ private_class_method :slice_collection_for_iteration_using_each
104
+
105
+ def self.range_method_overridden?(collection, method_name)
106
+ collection.method(method_name).owner != Range.instance_method(method_name).owner
107
+ end
108
+ private_class_method :range_method_overridden?
109
+
41
110
  def self.to_integer(num)
42
111
  return num if num.is_a?(Integer)
43
112
  num = num.to_s
@@ -37,6 +37,10 @@ module Liquid
37
37
  @markup
38
38
  end
39
39
 
40
+ def ==(other)
41
+ self.class == other.class && name == other.name && filters == other.filters
42
+ end
43
+
40
44
  def markup_context(markup)
41
45
  "in \"{{#{markup}}}\""
42
46
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.12.0"
5
+ VERSION = "5.14.0"
6
6
  end
data/lib/liquid.rb CHANGED
@@ -65,6 +65,7 @@ require 'liquid/lexer'
65
65
  require 'liquid/parser'
66
66
  require 'liquid/i18n'
67
67
  require 'liquid/drop'
68
+ require 'liquid/self_drop'
68
69
  require 'liquid/tablerowloop_drop'
69
70
  require 'liquid/forloop_drop'
70
71
  require 'liquid/extensions'
@@ -82,6 +83,7 @@ require 'liquid/resource_limits'
82
83
  require 'liquid/expression'
83
84
  require 'liquid/template'
84
85
  require 'liquid/condition'
86
+ require 'liquid/range_slice'
85
87
  require 'liquid/utils'
86
88
  require 'liquid/tokenizer'
87
89
  require 'liquid/parse_context'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.12.0
4
+ version: 5.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
@@ -103,8 +103,10 @@ files:
103
103
  - lib/liquid/profiler.rb
104
104
  - lib/liquid/profiler/hooks.rb
105
105
  - lib/liquid/range_lookup.rb
106
+ - lib/liquid/range_slice.rb
106
107
  - lib/liquid/registers.rb
107
108
  - lib/liquid/resource_limits.rb
109
+ - lib/liquid/self_drop.rb
108
110
  - lib/liquid/standardfilters.rb
109
111
  - lib/liquid/strainer_template.rb
110
112
  - lib/liquid/tablerowloop_drop.rb
@@ -159,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
161
  - !ruby/object:Gem::Version
160
162
  version: 1.3.7
161
163
  requirements: []
162
- rubygems_version: 4.0.8
164
+ rubygems_version: 4.0.21
163
165
  specification_version: 4
164
166
  summary: A secure, non-evaling end user template engine with aesthetic markup.
165
167
  test_files: []