liquid 5.4.0 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8408df245a1cc22ee1154fe5387e3e41eb3c740f7277e7d3736c1863d387e47
4
- data.tar.gz: 64549a58828fd7e9e0eb7310cb4371e75e64a7c3249fe9ebd021039e3334bba1
3
+ metadata.gz: 4d5699469b4a46eb15532bf2d5000925eeeff4f2eb5fef683d53bf09f0ec8825
4
+ data.tar.gz: df2bebc1a371dc250c768d46996c861fc22db9a1e2afc77f2f12247baa9a2e71
5
5
  SHA512:
6
- metadata.gz: 29aaff16e3bc464712cdcac3aa205df943132198ef9568d46f4a123d327849f7ead7bd669a095eae0425581eb59ea08874ac38664bf9d8df00b0aa99917c7768
7
- data.tar.gz: 9f6070c39733b7f4064f70676b1f886fc61070f3ba8921360b9c5fcdb09c217b3e58c3d59d8293b24b6c18e55899ed17552a2f867ea8eb95e7dee9a7deb29ae5
6
+ metadata.gz: e953d07e402f2325e56953a1cce6f4d334a183548fd70db1a472695b491f3f2efb6318625c1582e708362f5d2cff81f932d9c0d30c3da23d63fdfdab12b50725
7
+ data.tar.gz: d6abf81f05029169f17da65b9c93af90c96c93fccd6179cff3a02d050f5736e177c2686348b1d62af4caa432e6332ffc19c37c86004d139e422876049856fc97
data/History.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 5.5.0 2024-03-21
4
+
5
+ Please reference the GitHub release for more information.
6
+
3
7
  ## 5.4.0 2022-07-29
4
8
 
5
9
  ### Breaking Changes
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
- [![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
1
+ [![Build status](https://github.com/Shopify/liquid/actions/workflows/liquid.yml/badge.svg)](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
2
2
  [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid)
3
3
 
4
4
  # Liquid template engine
5
5
 
6
6
  * [Contributing guidelines](CONTRIBUTING.md)
7
7
  * [Version history](History.md)
8
- * [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
8
+ * [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
9
9
  * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
10
10
  * [Website](http://liquidmarkup.org/)
11
11
 
@@ -111,4 +111,4 @@ template.render!({ 'x' => 1}, { strict_variables: true })
111
111
 
112
112
  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.
113
113
 
114
- Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
114
+ Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
data/lib/liquid/block.rb CHANGED
@@ -36,13 +36,17 @@ module Liquid
36
36
  # @api private
37
37
  def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
38
38
  if tag == 'else'
39
- raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
40
- block_name: block_name)
39
+ raise SyntaxError, parse_context.locale.t(
40
+ "errors.syntax.unexpected_else",
41
+ block_name: block_name,
42
+ )
41
43
  elsif tag.start_with?('end')
42
- raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
44
+ raise SyntaxError, parse_context.locale.t(
45
+ "errors.syntax.invalid_delimiter",
43
46
  tag: tag,
44
47
  block_name: block_name,
45
- block_delimiter: block_delimiter)
48
+ block_delimiter: block_delimiter,
49
+ )
46
50
  else
47
51
  raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
48
52
  end
@@ -6,6 +6,7 @@ module Liquid
6
6
  class BlockBody
7
7
  LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
8
8
  FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
9
10
  ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
10
11
  WhitespaceOrNothing = /\A\s*\z/
11
12
  TAGSTART = "{%"
@@ -45,6 +46,12 @@ module Liquid
45
46
  end
46
47
  tag_name = Regexp.last_match(1)
47
48
  markup = Regexp.last_match(2)
49
+
50
+ if tag_name == 'liquid'
51
+ parse_context.line_number -= 1
52
+ next parse_liquid_tag(markup, parse_context)
53
+ end
54
+
48
55
  unless (tag = registered_tags[tag_name])
49
56
  # end parsing if we reach an unknown tag and let the caller decide
50
57
  # determine how to proceed
@@ -109,14 +116,22 @@ module Liquid
109
116
  end
110
117
  end
111
118
 
112
- private def parse_for_document(tokenizer, parse_context)
119
+ private def handle_invalid_tag_token(token, parse_context)
120
+ if token.end_with?('%}')
121
+ yield token, token
122
+ else
123
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
124
+ end
125
+ end
126
+
127
+ private def parse_for_document(tokenizer, parse_context, &block)
113
128
  while (token = tokenizer.shift)
114
129
  next if token.empty?
115
130
  case
116
131
  when token.start_with?(TAGSTART)
117
132
  whitespace_handler(token, parse_context)
118
133
  unless token =~ FullToken
119
- BlockBody.raise_missing_tag_terminator(token, parse_context)
134
+ return handle_invalid_tag_token(token, parse_context, &block)
120
135
  end
121
136
  tag_name = Regexp.last_match(2)
122
137
  markup = Regexp.last_match(4)
@@ -24,6 +24,9 @@ module Liquid
24
24
  else
25
25
  false
26
26
  end
27
+ rescue Encoding::CompatibilityError
28
+ # "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
29
+ left.b.include?(right.b)
27
30
  end,
28
31
  }
29
32
 
@@ -69,9 +72,9 @@ module Liquid
69
72
 
70
73
  case condition.child_relation
71
74
  when :or
72
- break if result
75
+ break if Liquid::Utils.to_liquid_value(result)
73
76
  when :and
74
- break unless result
77
+ break unless Liquid::Utils.to_liquid_value(result)
75
78
  else
76
79
  break
77
80
  end
@@ -159,8 +162,10 @@ module Liquid
159
162
  class ParseTreeVisitor < Liquid::ParseTreeVisitor
160
163
  def children
161
164
  [
162
- @node.left, @node.right,
163
- @node.child_condition, @node.attachment
165
+ @node.left,
166
+ @node.right,
167
+ @node.child_condition,
168
+ @node.attachment
164
169
  ].compact
165
170
  end
166
171
  end
@@ -26,7 +26,7 @@ module Liquid
26
26
  @environments = [environments]
27
27
  @environments.flatten!
28
28
 
29
- @static_environments = [static_environments].flat_map(&:freeze).freeze
29
+ @static_environments = [static_environments].flatten(1).freeze
30
30
  @scopes = [(outer_scope || {})]
31
31
  @registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
32
32
  @errors = []
@@ -144,7 +144,7 @@ module Liquid
144
144
  self.class.build(
145
145
  resource_limits: resource_limits,
146
146
  static_environments: static_environments,
147
- registers: Registers.new(registers)
147
+ registers: Registers.new(registers),
148
148
  ).tap do |subcontext|
149
149
  subcontext.base_scope_depth = base_scope_depth + 1
150
150
  subcontext.exception_renderer = exception_renderer
@@ -197,10 +197,14 @@ module Liquid
197
197
  try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
198
198
  end
199
199
 
200
- variable = variable.to_liquid
200
+ # update variable's context before invoking #to_liquid
201
201
  variable.context = self if variable.respond_to?(:context=)
202
202
 
203
- variable
203
+ liquid_variable = variable.to_liquid
204
+
205
+ liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
206
+
207
+ liquid_variable
204
208
  end
205
209
 
206
210
  def lookup_and_evaluate(obj, key, raise_on_not_found: true)
data/lib/liquid/drop.rb CHANGED
@@ -25,6 +25,10 @@ module Liquid
25
25
  class Drop
26
26
  attr_writer :context
27
27
 
28
+ def initialize
29
+ @context = nil
30
+ end
31
+
28
32
  # Catch all for the method
29
33
  def liquid_method_missing(method)
30
34
  return nil unless @context&.strict_variables
data/lib/liquid/errors.rb CHANGED
@@ -40,19 +40,20 @@ module Liquid
40
40
  end
41
41
  end
42
42
 
43
- ArgumentError = Class.new(Error)
44
- ContextError = Class.new(Error)
45
- FileSystemError = Class.new(Error)
46
- StandardError = Class.new(Error)
47
- SyntaxError = Class.new(Error)
48
- StackLevelError = Class.new(Error)
49
- MemoryError = Class.new(Error)
50
- ZeroDivisionError = Class.new(Error)
51
- FloatDomainError = Class.new(Error)
52
- UndefinedVariable = Class.new(Error)
53
- UndefinedDropMethod = Class.new(Error)
54
- UndefinedFilter = Class.new(Error)
55
- MethodOverrideError = Class.new(Error)
56
- DisabledError = Class.new(Error)
57
- InternalError = Class.new(Error)
43
+ ArgumentError = Class.new(Error)
44
+ ContextError = Class.new(Error)
45
+ FileSystemError = Class.new(Error)
46
+ StandardError = Class.new(Error)
47
+ SyntaxError = Class.new(Error)
48
+ StackLevelError = Class.new(Error)
49
+ MemoryError = Class.new(Error)
50
+ ZeroDivisionError = Class.new(Error)
51
+ FloatDomainError = Class.new(Error)
52
+ UndefinedVariable = Class.new(Error)
53
+ UndefinedDropMethod = Class.new(Error)
54
+ UndefinedFilter = Class.new(Error)
55
+ MethodOverrideError = Class.new(Error)
56
+ DisabledError = Class.new(Error)
57
+ InternalError = Class.new(Error)
58
+ TemplateEncodingError = Class.new(Error)
58
59
  end
@@ -3,7 +3,10 @@
3
3
  module Liquid
4
4
  class Expression
5
5
  LITERALS = {
6
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
6
+ nil => nil,
7
+ 'nil' => nil,
8
+ 'null' => nil,
9
+ '' => nil,
7
10
  'true' => true,
8
11
  'false' => false,
9
12
  'blank' => '',
@@ -5,7 +5,7 @@ module Liquid
5
5
  # @liquid_type object
6
6
  # @liquid_name forloop
7
7
  # @liquid_summary
8
- # Information about a parent [`for` loop](/api/liquid/tags#for).
8
+ # Information about a parent [`for` loop](/docs/api/liquid/tags/for).
9
9
  class ForloopDrop < Drop
10
10
  def initialize(name, length, parentloop)
11
11
  @name = name
@@ -30,10 +30,7 @@ module Liquid
30
30
  # @liquid_return [forloop]
31
31
  attr_reader :parentloop
32
32
 
33
- def name
34
- Usage.increment('forloop_drop_name')
35
- @name
36
- end
33
+ attr_reader :name
37
34
 
38
35
  # @liquid_public_docs
39
36
  # @liquid_summary
data/lib/liquid/lexer.rb CHANGED
@@ -18,6 +18,7 @@ module Liquid
18
18
  IDENTIFIER = /[a-zA-Z_][\w-]*\??/
19
19
  SINGLE_STRING_LITERAL = /'[^\']*'/
20
20
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
21
+ STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
21
22
  NUMBER_LITERAL = /-?\d+(\.\d+)?/
22
23
  DOTDOT = /\.\./
23
24
  COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
@@ -35,9 +36,7 @@ module Liquid
35
36
  break if @ss.eos?
36
37
  tok = if (t = @ss.scan(COMPARISON_OPERATOR))
37
38
  [:comparison, t]
38
- elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
39
- [:string, t]
40
- elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
39
+ elsif (t = @ss.scan(STRING_LITERAL))
41
40
  [:string, t]
42
41
  elsif (t = @ss.scan(NUMBER_LITERAL))
43
42
  [:number, t]
@@ -15,6 +15,7 @@
15
15
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
16
  inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
17
17
  invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
18
+ invalid_template_encoding: "Invalid template encoding"
18
19
  render: "Syntax error in tag 'render' - Template name must be a quoted string"
19
20
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
20
21
  tag_never_closed: "'%{block_name}' tag was never closed"
@@ -4,7 +4,8 @@ module Liquid
4
4
  class PartialCache
5
5
  def self.load(template_name, context:, parse_context:)
6
6
  cached_partials = context.registers[:cached_partials]
7
- cached = cached_partials[template_name]
7
+ cache_key = "#{template_name}:#{parse_context.error_mode}"
8
+ cached = cached_partials[cache_key]
8
9
  return cached if cached
9
10
 
10
11
  file_system = context.registers[:file_system]
@@ -15,8 +16,16 @@ module Liquid
15
16
  template_factory = context.registers[:template_factory]
16
17
  template = template_factory.for(template_name)
17
18
 
18
- partial = template.parse(source, parse_context)
19
- cached_partials[template_name] = partial
19
+ begin
20
+ partial = template.parse(source, parse_context)
21
+ rescue Liquid::Error => e
22
+ e.template_name = template&.name || template_name
23
+ raise e
24
+ end
25
+
26
+ partial.name ||= template_name
27
+
28
+ cached_partials[cache_key] = partial
20
29
  ensure
21
30
  parse_context.partial = false
22
31
  end
@@ -8,7 +8,17 @@ module Liquid
8
8
  if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
9
9
  new(start_obj, end_obj)
10
10
  else
11
- start_obj.to_i..end_obj.to_i
11
+ begin
12
+ start_obj.to_i..end_obj.to_i
13
+ rescue NoMethodError
14
+ invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
15
+ invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
16
+ if invalid_expr
17
+ raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
18
+ end
19
+
20
+ raise
21
+ end
12
22
  end
13
23
  end
14
24
 
@@ -6,7 +6,14 @@ require 'bigdecimal'
6
6
 
7
7
  module Liquid
8
8
  module StandardFilters
9
- MAX_INT = (1 << 31) - 1
9
+ MAX_I32 = (1 << 31) - 1
10
+ private_constant :MAX_I32
11
+
12
+ MIN_I64 = -(1 << 63)
13
+ MAX_I64 = (1 << 63) - 1
14
+ I64_RANGE = MIN_I64..MAX_I64
15
+ private_constant :MIN_I64, :MAX_I64, :I64_RANGE
16
+
10
17
  HTML_ESCAPE = {
11
18
  '&' => '&amp;',
12
19
  '>' => '&gt;',
@@ -18,10 +25,23 @@ module Liquid
18
25
  STRIP_HTML_BLOCKS = Regexp.union(
19
26
  %r{<script.*?</script>}m,
20
27
  /<!--.*?-->/m,
21
- %r{<style.*?</style>}m
28
+ %r{<style.*?</style>}m,
22
29
  )
23
30
  STRIP_HTML_TAGS = /<.*?>/m
24
31
 
32
+ class << self
33
+ def try_coerce_encoding(input, encoding:)
34
+ original_encoding = input.encoding
35
+ if input.encoding != encoding
36
+ input.force_encoding(encoding)
37
+ unless input.valid_encoding?
38
+ input.force_encoding(original_encoding)
39
+ end
40
+ end
41
+ input
42
+ end
43
+ end
44
+
25
45
  # @liquid_public_docs
26
46
  # @liquid_type filter
27
47
  # @liquid_category array
@@ -62,7 +82,7 @@ module Liquid
62
82
  # @liquid_type filter
63
83
  # @liquid_category string
64
84
  # @liquid_summary
65
- # Capitalizes the first word in a string.
85
+ # Capitalizes the first word in a string and downcases the remaining characters.
66
86
  # @liquid_syntax string | capitalize
67
87
  # @liquid_return [string]
68
88
  def capitalize(input)
@@ -73,7 +93,7 @@ module Liquid
73
93
  # @liquid_type filter
74
94
  # @liquid_category string
75
95
  # @liquid_summary
76
- # Escapes a string.
96
+ # Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that don’t have a corresponding escape sequence.".
77
97
  # @liquid_syntax string | escape
78
98
  # @liquid_return [string]
79
99
  def escape(input)
@@ -143,7 +163,8 @@ module Liquid
143
163
  # @liquid_syntax string | base64_decode
144
164
  # @liquid_return [string]
145
165
  def base64_decode(input)
146
- Base64.strict_decode64(input.to_s)
166
+ input = input.to_s
167
+ StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
147
168
  rescue ::ArgumentError
148
169
  raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
149
170
  end
@@ -167,7 +188,8 @@ module Liquid
167
188
  # @liquid_syntax string | base64_url_safe_decode
168
189
  # @liquid_return [string]
169
190
  def base64_url_safe_decode(input)
170
- Base64.urlsafe_decode64(input.to_s)
191
+ input = input.to_s
192
+ StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
171
193
  rescue ::ArgumentError
172
194
  raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
173
195
  end
@@ -186,10 +208,19 @@ module Liquid
186
208
  offset = Utils.to_integer(offset)
187
209
  length = length ? Utils.to_integer(length) : 1
188
210
 
189
- if input.is_a?(Array)
190
- input.slice(offset, length) || []
191
- else
192
- input.to_s.slice(offset, length) || ''
211
+ begin
212
+ if input.is_a?(Array)
213
+ input.slice(offset, length) || []
214
+ else
215
+ input.to_s.slice(offset, length) || ''
216
+ end
217
+ rescue RangeError
218
+ if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
219
+ raise # unexpected error
220
+ end
221
+ offset = offset.clamp(I64_RANGE)
222
+ length = length.clamp(I64_RANGE)
223
+ retry
193
224
  end
194
225
  end
195
226
 
@@ -239,9 +270,9 @@ module Liquid
239
270
  wordlist = begin
240
271
  input.split(" ", words + 1)
241
272
  rescue RangeError
242
- raise if words + 1 < MAX_INT
243
- # e.g. integer #{words} too big to convert to `int'
244
- raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
273
+ # integer too big for String#split, but we can semantically assume no truncation is needed
274
+ return input if words + 1 > MAX_I32
275
+ raise # unexpected error
245
276
  end
246
277
  return input if wordlist.length <= words
247
278
 
@@ -599,7 +630,7 @@ module Liquid
599
630
  # @liquid_description
600
631
  # > Note:
601
632
  # > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
602
- # > [`uniq` filter](/api/liquid/filters#uniq).
633
+ # > [`uniq` filter](/docs/api/liquid/filters/uniq).
603
634
  # @liquid_syntax array | concat: array
604
635
  # @liquid_return [array[untyped]]
605
636
  def concat(input, array)
@@ -741,7 +772,7 @@ module Liquid
741
772
  # @liquid_type filter
742
773
  # @liquid_category math
743
774
  # @liquid_summary
744
- # Divides a number by a given number.
775
+ # Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float.
745
776
  # @liquid_syntax number | divided_by: number
746
777
  # @liquid_return [number]
747
778
  def divided_by(input, operand)
@@ -841,9 +872,9 @@ module Liquid
841
872
  # @liquid_summary
842
873
  # Sets a default value for any variable whose value is one of the following:
843
874
  #
844
- # - [`empty`](/api/liquid/basics#empty)
845
- # - [`false`](/api/liquid/basics#truthy-and-falsy)
846
- # - [`nil`](/api/liquid/basics#nil)
875
+ # - [`empty`](/docs/api/liquid/basics#empty)
876
+ # - [`false`](/docs/api/liquid/basics#truthy-and-falsy)
877
+ # - [`nil`](/docs/api/liquid/basics#nil)
847
878
  # @liquid_syntax variable | default: variable
848
879
  # @liquid_return [untyped]
849
880
  # @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
@@ -853,6 +884,36 @@ module Liquid
853
884
  false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
854
885
  end
855
886
 
887
+ # @liquid_public_docs
888
+ # @liquid_type filter
889
+ # @liquid_category array
890
+ # @liquid_summary
891
+ # Returns the sum of all elements in an array.
892
+ # @liquid_syntax array | sum
893
+ # @liquid_return [number]
894
+ def sum(input, property = nil)
895
+ ary = InputIterator.new(input, context)
896
+ return 0 if ary.empty?
897
+
898
+ values_for_sum = ary.map do |item|
899
+ if property.nil?
900
+ item
901
+ elsif item.respond_to?(:[])
902
+ item[property]
903
+ else
904
+ 0
905
+ end
906
+ rescue TypeError
907
+ raise_property_error(property)
908
+ end
909
+
910
+ result = InputIterator.new(values_for_sum, context).sum do |item|
911
+ Utils.to_number(item)
912
+ end
913
+
914
+ result.is_a?(BigDecimal) ? result.to_f : result
915
+ end
916
+
856
917
  private
857
918
 
858
919
  attr_reader :context
@@ -883,6 +944,8 @@ module Liquid
883
944
  def nil_safe_casecmp(a, b)
884
945
  if !a.nil? && !b.nil?
885
946
  a.to_s.casecmp(b.to_s)
947
+ elsif a.nil? && b.nil?
948
+ 0
886
949
  else
887
950
  a.nil? ? 1 : -1
888
951
  end
@@ -5,7 +5,7 @@ module Liquid
5
5
  # @liquid_type object
6
6
  # @liquid_name tablerowloop
7
7
  # @liquid_summary
8
- # Information about a parent [`tablerow` loop](/api/liquid/tags#tablerow).
8
+ # Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
9
9
  class TablerowloopDrop < Drop
10
10
  def initialize(length, cols)
11
11
  @length = length
@@ -3,14 +3,6 @@
3
3
  module Liquid
4
4
  class Tag
5
5
  module Disabler
6
- module ClassMethods
7
- attr_reader :disabled_tags
8
- end
9
-
10
- def self.prepended(base)
11
- base.extend(ClassMethods)
12
- end
13
-
14
6
  def render_to_output_buffer(context, output)
15
7
  context.with_disabled_tags(self.class.disabled_tags) do
16
8
  super
data/lib/liquid/tag.rb CHANGED
@@ -14,12 +14,18 @@ module Liquid
14
14
  end
15
15
 
16
16
  def disable_tags(*tag_names)
17
- @disabled_tags ||= []
18
- @disabled_tags.concat(tag_names)
17
+ tag_names += disabled_tags
18
+ define_singleton_method(:disabled_tags) { tag_names }
19
19
  prepend(Disabler)
20
20
  end
21
21
 
22
22
  private :new
23
+
24
+ protected
25
+
26
+ def disabled_tags
27
+ []
28
+ end
23
29
  end
24
30
 
25
31
  def initialize(tag_name, markup, parse_context)
@@ -48,7 +54,8 @@ module Liquid
48
54
  # of the `render_to_output_buffer` method will become the default and the `render`
49
55
  # method will be removed.
50
56
  def render_to_output_buffer(context, output)
51
- output << render(context)
57
+ render_result = render(context)
58
+ output << render_result if render_result
52
59
  output
53
60
  end
54
61
 
@@ -8,7 +8,7 @@ module Liquid
8
8
  # @liquid_summary
9
9
  # Creates a new variable.
10
10
  # @liquid_description
11
- # You can create variables of any [basic type](/api/liquid/basics#types), [object](/api/liquid/objects), or object property.
11
+ # You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
12
12
  # @liquid_syntax
13
13
  # {% assign variable_name = value %}
14
14
  # @liquid_syntax_keyword variable_name The name of the variable being created.
@@ -15,7 +15,7 @@ module Liquid
15
15
  # @liquid_category iteration
16
16
  # @liquid_name break
17
17
  # @liquid_summary
18
- # Stops a [`for` loop](/api/liquid/tags#for) from iterating.
18
+ # Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
19
19
  # @liquid_syntax
20
20
  # {% break %}
21
21
  class Break < Tag
@@ -77,7 +77,7 @@ module Liquid
77
77
  end
78
78
 
79
79
  result = Liquid::Utils.to_liquid_value(
80
- block.evaluate(context)
80
+ block.evaluate(context),
81
81
  )
82
82
 
83
83
  if result
@@ -8,7 +8,7 @@ module Liquid
8
8
  # @liquid_summary
9
9
  # Prevents an expression from being rendered or output.
10
10
  # @liquid_description
11
- # Any text inside `comment` tags won't be output, and any Liquid code won't be rendered.
11
+ # Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
12
12
  # @liquid_syntax
13
13
  # {% comment %}
14
14
  # content
@@ -25,6 +25,65 @@ module Liquid
25
25
  def blank?
26
26
  true
27
27
  end
28
+
29
+ private
30
+
31
+ def parse_body(body, tokenizer)
32
+ if parse_context.depth >= MAX_DEPTH
33
+ raise StackLevelError, "Nesting too deep"
34
+ end
35
+
36
+ parse_context.depth += 1
37
+ comment_tag_depth = 1
38
+
39
+ begin
40
+ # Consume tokens without creating child nodes.
41
+ # The children tag doesn't require to be a valid Liquid except the comment and raw tag.
42
+ # The child comment and raw tag must be closed.
43
+ while (token = tokenizer.send(:shift))
44
+ tag_name = if tokenizer.for_liquid_tag
45
+ next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
46
+
47
+ tag_name_match = BlockBody::LiquidTagToken.match(token)
48
+
49
+ next if tag_name_match.nil?
50
+
51
+ tag_name_match[1]
52
+ else
53
+ token =~ BlockBody::FullToken
54
+ Regexp.last_match(2)
55
+ end
56
+
57
+ case tag_name
58
+ when "raw"
59
+ parse_raw_tag_body(tokenizer)
60
+ when "comment"
61
+ comment_tag_depth += 1
62
+ when "endcomment"
63
+ comment_tag_depth -= 1
64
+ end
65
+
66
+ if comment_tag_depth.zero?
67
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
68
+ return false
69
+ end
70
+ end
71
+
72
+ raise_tag_never_closed(block_name)
73
+ ensure
74
+ parse_context.depth -= 1
75
+ end
76
+
77
+ false
78
+ end
79
+
80
+ def parse_raw_tag_body(tokenizer)
81
+ while (token = tokenizer.send(:shift))
82
+ return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
83
+ end
84
+
85
+ raise_tag_never_closed("raw")
86
+ end
28
87
  end
29
88
 
30
89
  Template.register_tag('comment', Comment)
@@ -6,7 +6,7 @@ module Liquid
6
6
  # @liquid_category iteration
7
7
  # @liquid_name continue
8
8
  # @liquid_summary
9
- # Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration.
9
+ # Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
10
10
  # @liquid_syntax
11
11
  # {% continue %}
12
12
  class Continue < Tag
@@ -6,7 +6,7 @@ module Liquid
6
6
  # @liquid_category iteration
7
7
  # @liquid_name cycle
8
8
  # @liquid_summary
9
- # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for).
9
+ # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for).
10
10
  # @liquid_description
11
11
  # The `cycle` tag must be used inside a `for` loop.
12
12
  #
@@ -12,22 +12,25 @@ module Liquid
12
12
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
13
  # [snippets](/themes/architecture#snippets) included in the file.
14
14
  #
15
- # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign)
16
- # and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/api/liquid/tags#increment) share
15
+ # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
16
+ # and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
17
17
  # variables.
18
18
  # @liquid_syntax
19
19
  # {% decrement variable_name %}
20
20
  # @liquid_syntax_keyword variable_name The name of the variable being decremented.
21
21
  class Decrement < Tag
22
+ attr_reader :variable_name
23
+
22
24
  def initialize(tag_name, markup, options)
23
25
  super
24
- @variable = markup.strip
26
+ @variable_name = markup.strip
25
27
  end
26
28
 
27
29
  def render_to_output_buffer(context, output)
28
- value = context.environments.first[@variable] ||= 0
30
+ counter_environment = context.environments.first
31
+ value = counter_environment[@variable_name] || 0
29
32
  value -= 1
30
- context.environments.first[@variable] = value
33
+ counter_environment[@variable_name] = value
31
34
  output << value.to_s
32
35
  output
33
36
  end
@@ -9,10 +9,10 @@ module Liquid
9
9
  # Outputs an expression.
10
10
  # @liquid_description
11
11
  # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
12
- # bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid).
12
+ # bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid).
13
13
  #
14
14
  # > Tip:
15
- # > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
15
+ # > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
16
16
  # @liquid_syntax
17
17
  # {% liquid
18
18
  # echo expression
@@ -9,10 +9,10 @@ module Liquid
9
9
  # Renders an expression for every item in an array.
10
10
  # @liquid_description
11
11
  # You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
12
- # [`paginate` tag](/api/liquid/tags#paginate) to split the items over multiple pages.
12
+ # [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
13
13
  #
14
14
  # > Tip:
15
- # > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop.
15
+ # > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
16
16
  # @liquid_syntax
17
17
  # {% for variable in array %}
18
18
  # expression
@@ -98,11 +98,12 @@ module Liquid
98
98
  @name = "#{@variable_name}-#{collection_name}"
99
99
  @reversed = p.id?('reversed')
100
100
 
101
- while p.look(:id) && p.look(:colon, 1)
101
+ while p.look(:comma) || p.look(:id)
102
+ p.consume?(:comma)
102
103
  unless (attribute = p.id?('limit') || p.id?('offset'))
103
104
  raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
104
105
  end
105
- p.consume
106
+ p.consume(:colon)
106
107
  set_attribute(attribute, p.expression)
107
108
  end
108
109
  p.consume(:end_of_string)
@@ -177,7 +178,6 @@ module Liquid
177
178
  case key
178
179
  when 'offset'
179
180
  @from = if expr == 'continue'
180
- Usage.increment('for_offset_continue')
181
181
  :continue
182
182
  else
183
183
  parse_expression(expr)
@@ -53,7 +53,7 @@ module Liquid
53
53
  def render_to_output_buffer(context, output)
54
54
  @blocks.each do |block|
55
55
  result = Liquid::Utils.to_liquid_value(
56
- block.evaluate(context)
56
+ block.evaluate(context),
57
57
  )
58
58
 
59
59
  if result
@@ -8,7 +8,7 @@ module Liquid
8
8
  # @liquid_summary
9
9
  # Renders a [snippet](/themes/architecture#snippets).
10
10
  # @liquid_description
11
- # Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags#variable-tags) outside of the
11
+ # Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
12
12
  # snippet.
13
13
  # @liquid_syntax
14
14
  # {% include 'filename' %}
@@ -16,7 +16,7 @@ module Liquid
16
16
  # @liquid_deprecated
17
17
  # Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
18
18
  #
19
- # The `include` tag has been replaced by [`render`](/api/liquid/tags#render).
19
+ # The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
20
20
  class Include < Tag
21
21
  prepend Tag::Disableable
22
22
 
@@ -52,12 +52,12 @@ module Liquid
52
52
 
53
53
  def render_to_output_buffer(context, output)
54
54
  template_name = context.evaluate(@template_name_expr)
55
- raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
55
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
56
56
 
57
57
  partial = PartialCache.load(
58
58
  template_name,
59
59
  context: context,
60
- parse_context: parse_context
60
+ parse_context: parse_context,
61
61
  )
62
62
 
63
63
  context_variable_name = @alias_name || template_name.split('/').last
@@ -70,9 +70,11 @@ module Liquid
70
70
 
71
71
  old_template_name = context.template_name
72
72
  old_partial = context.partial
73
+
73
74
  begin
74
- context.template_name = template_name
75
- context.partial = true
75
+ context.template_name = partial.name
76
+ context.partial = true
77
+
76
78
  context.stack do
77
79
  @attributes.each do |key, value|
78
80
  context[key] = context.evaluate(value)
@@ -12,21 +12,24 @@ module Liquid
12
12
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
13
  # [snippets](/themes/architecture#snippets) included in the file.
14
14
  #
15
- # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags#assign)
16
- # and [`capture`](/api/liquid/tags#capture). However, `increment` and [`decrement`](/api/liquid/tags#decrement) share
15
+ # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
16
+ # and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
17
17
  # variables.
18
18
  # @liquid_syntax
19
19
  # {% increment variable_name %}
20
20
  # @liquid_syntax_keyword variable_name The name of the variable being incremented.
21
21
  class Increment < Tag
22
+ attr_reader :variable_name
23
+
22
24
  def initialize(tag_name, markup, options)
23
25
  super
24
- @variable = markup.strip
26
+ @variable_name = markup.strip
25
27
  end
26
28
 
27
29
  def render_to_output_buffer(context, output)
28
- value = context.environments.first[@variable] ||= 0
29
- context.environments.first[@variable] = value + 1
30
+ counter_environment = context.environments.first
31
+ value = counter_environment[@variable_name] || 0
32
+ counter_environment[@variable_name] = value + 1
30
33
 
31
34
  output << value.to_s
32
35
  output
@@ -1,19 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liquid
4
- # @liquid_public_docs
5
- # @liquid_type tag
6
- # @liquid_category syntax
7
- # @liquid_name inline_comment
8
- # @liquid_summary
9
- # Prevents an expression from being rendered or output.
10
- # @liquid_description
11
- # Any text inside an `inline_comment` tag won't be rendered or output.
12
- #
13
- # You can create multi-line inline comments. However, each line must begin with a `#`.
14
- # @liquid_syntax
15
- # {% # content %}
16
- # @liquid_syntax_keyword content The content of the comment.
17
4
  class InlineComment < Tag
18
5
  def initialize(tag_name, markup, options)
19
6
  super
@@ -14,7 +14,6 @@ module Liquid
14
14
  # @liquid_syntax_keyword expression The expression to be output without being rendered.
15
15
  class Raw < Block
16
16
  Syntax = /\A\s*\z/
17
- FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
18
17
 
19
18
  def initialize(tag_name, markup, parse_context)
20
19
  super
@@ -25,7 +24,8 @@ module Liquid
25
24
  def parse(tokens)
26
25
  @body = +''
27
26
  while (token = tokens.shift)
28
- if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
27
+ if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
28
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
29
29
  @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
30
30
  return
31
31
  end
@@ -8,19 +8,19 @@ module Liquid
8
8
  # @liquid_summary
9
9
  # Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
10
10
  # @liquid_description
11
- # Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags#variable-tags) outside
12
- # of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags#render-passing-variables-to-snippets)
11
+ # Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
12
+ # of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
13
13
  # to pass outside variables to snippets.
14
14
  #
15
15
  # While you can't directly access created variables, you can access global objects, as well as any objects that are
16
16
  # directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
17
- # can access the [`product` object](/api/liquid/objects#product), and a snippet or app block inside a [section](/themes/architecture/sections)
18
- # can access the [`section` object](/api/liquid/objects#section).
17
+ # can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
18
+ # can access the [`section` object](/docs/api/liquid/objects/section).
19
19
  #
20
20
  # Outside a snippet or app block, you can't access variables created inside the snippet or app block.
21
21
  #
22
22
  # > Note:
23
- # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags#include)
23
+ # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
24
24
  # > inside the snippet.
25
25
  # @liquid_syntax
26
26
  # {% render 'filename' %}
@@ -31,7 +31,7 @@ module Liquid
31
31
 
32
32
  disable_tags "include"
33
33
 
34
- attr_reader :template_name_expr, :variable_name_expr, :attributes
34
+ attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
35
35
 
36
36
  def initialize(tag_name, markup, options)
37
37
  super
@@ -45,7 +45,7 @@ module Liquid
45
45
  @alias_name = Regexp.last_match(6)
46
46
  @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
47
47
  @template_name_expr = parse_expression(template_name)
48
- @for = (with_or_for == FOR)
48
+ @is_for_loop = (with_or_for == FOR)
49
49
 
50
50
  @attributes = {}
51
51
  markup.scan(TagAttributes) do |key, value|
@@ -53,6 +53,10 @@ module Liquid
53
53
  end
54
54
  end
55
55
 
56
+ def for_loop?
57
+ @is_for_loop
58
+ end
59
+
56
60
  def render_to_output_buffer(context, output)
57
61
  render_tag(context, output)
58
62
  end
@@ -65,14 +69,14 @@ module Liquid
65
69
  partial = PartialCache.load(
66
70
  template_name,
67
71
  context: context,
68
- parse_context: parse_context
72
+ parse_context: parse_context,
69
73
  )
70
74
 
71
75
  context_variable_name = @alias_name || template_name.split('/').last
72
76
 
73
77
  render_partial_func = ->(var, forloop) {
74
78
  inner_context = context.new_isolated_subcontext
75
- inner_context.template_name = template_name
79
+ inner_context.template_name = partial.name
76
80
  inner_context.partial = true
77
81
  inner_context['forloop'] = forloop if forloop
78
82
 
@@ -85,7 +89,7 @@ module Liquid
85
89
  }
86
90
 
87
91
  variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
88
- if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
92
+ if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
89
93
  forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
90
94
  variable.each { |var| render_partial_func.call(var, forloop) }
91
95
  else
@@ -11,7 +11,7 @@ module Liquid
11
11
  # The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
12
12
  #
13
13
  # > Tip:
14
- # > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects#tablerowloop) with information about the loop.
14
+ # > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
15
15
  # @liquid_syntax
16
16
  # {% tablerow variable in array %}
17
17
  # expression
@@ -45,13 +45,13 @@ module Liquid
45
45
  def render_to_output_buffer(context, output)
46
46
  (collection = context.evaluate(@collection_name)) || (return '')
47
47
 
48
- from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
49
- to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
48
+ from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
49
+ to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
50
50
 
51
51
  collection = Utils.slice_collection(collection, from, to)
52
52
  length = collection.length
53
53
 
54
- cols = context.evaluate(@attributes['cols']).to_i
54
+ cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
55
55
 
56
56
  output << "<tr class=\"row1\">\n"
57
57
  context.stack do
@@ -82,6 +82,14 @@ module Liquid
82
82
  super + @node.attributes.values + [@node.collection_name]
83
83
  end
84
84
  end
85
+
86
+ private
87
+
88
+ def to_integer(value)
89
+ value.to_i
90
+ rescue NoMethodError
91
+ raise Liquid::ArgumentError, "invalid integer"
92
+ end
85
93
  end
86
94
 
87
95
  Template.register_tag('tablerow', TableRow)
@@ -11,7 +11,7 @@ module Liquid
11
11
  # Renders an expression unless a specific condition is `true`.
12
12
  # @liquid_description
13
13
  # > Tip:
14
- # > Similar to the [`if` tag](/api/liquid/tags#if), you can use `elsif` to add more conditions to an `unless` tag.
14
+ # > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
15
15
  # @liquid_syntax
16
16
  # {% unless condition %}
17
17
  # expression
@@ -23,7 +23,7 @@ module Liquid
23
23
  # First condition is interpreted backwards ( if not )
24
24
  first_block = @blocks.first
25
25
  result = Liquid::Utils.to_liquid_value(
26
- first_block.evaluate(context)
26
+ first_block.evaluate(context),
27
27
  )
28
28
 
29
29
  unless result
@@ -33,7 +33,7 @@ module Liquid
33
33
  # After the first condition unless works just like if
34
34
  @blocks[1..-1].each do |block|
35
35
  result = Liquid::Utils.to_liquid_value(
36
- block.evaluate(context)
36
+ block.evaluate(context),
37
37
  )
38
38
 
39
39
  if result
@@ -15,7 +15,7 @@ module Liquid
15
15
  # template.render('user_name' => 'bob')
16
16
  #
17
17
  class Template
18
- attr_accessor :root
18
+ attr_accessor :root, :name
19
19
  attr_reader :resource_limits, :warnings
20
20
 
21
21
  class TagRegistry
@@ -107,6 +107,12 @@ module Liquid
107
107
  # Returns self for easy chaining
108
108
  def parse(source, options = {})
109
109
  parse_context = configure_options(options)
110
+ source = source.to_s.to_str
111
+
112
+ unless source.valid_encoding?
113
+ raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
114
+ end
115
+
110
116
  tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
111
117
  @root = Document.parse(tokenizer, parse_context)
112
118
  self
@@ -189,6 +195,8 @@ module Liquid
189
195
  @profiler = context.profiler = Liquid::Profiler.new
190
196
  end
191
197
 
198
+ context.template_name ||= name
199
+
192
200
  begin
193
201
  # render the nodelist.
194
202
  @root.render_to_output_buffer(context, output || +'')
@@ -5,14 +5,18 @@ module Liquid
5
5
  attr_reader :line_number, :for_liquid_tag
6
6
 
7
7
  def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
8
- @source = source.to_s.to_str
8
+ @source = source
9
9
  @line_number = line_number || (line_numbers ? 1 : nil)
10
10
  @for_liquid_tag = for_liquid_tag
11
+ @offset = 0
11
12
  @tokens = tokenize
12
13
  end
13
14
 
14
15
  def shift
15
- (token = @tokens.shift) || return
16
+ token = @tokens[@offset]
17
+ return nil unless token
18
+
19
+ @offset += 1
16
20
 
17
21
  if @line_number
18
22
  @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -31,7 +35,9 @@ module Liquid
31
35
  tokens = @source.split(TemplateParser)
32
36
 
33
37
  # removes the rogue empty element at the beginning of the array
34
- tokens.shift if tokens[0]&.empty?
38
+ if tokens[0]&.empty?
39
+ @offset += 1
40
+ end
35
41
 
36
42
  tokens
37
43
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.4.0"
5
+ VERSION = "5.5.0"
6
6
  end
data/lib/liquid.rb CHANGED
@@ -41,7 +41,7 @@ module Liquid
41
41
  AnyStartingTag = /#{TagStart}|#{VariableStart}/o
42
42
  PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
43
43
  TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
44
- VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
44
+ VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
45
45
 
46
46
  RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
47
47
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-29 00:00:00.000000000 Z
11
+ date: 2024-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  - !ruby/object:Gem::Version
131
131
  version: 1.3.7
132
132
  requirements: []
133
- rubygems_version: 3.3.3
133
+ rubygems_version: 3.5.6
134
134
  signing_key:
135
135
  specification_version: 4
136
136
  summary: A secure, non-evaling end user template engine with aesthetic markup.