liquid 5.6.0 → 5.7.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.
@@ -64,7 +64,7 @@ module Liquid
64
64
  # @liquid_syntax string | downcase
65
65
  # @liquid_return [string]
66
66
  def downcase(input)
67
- input.to_s.downcase
67
+ Utils.to_s(input).downcase
68
68
  end
69
69
 
70
70
  # @liquid_public_docs
@@ -75,7 +75,7 @@ module Liquid
75
75
  # @liquid_syntax string | upcase
76
76
  # @liquid_return [string]
77
77
  def upcase(input)
78
- input.to_s.upcase
78
+ Utils.to_s(input).upcase
79
79
  end
80
80
 
81
81
  # @liquid_public_docs
@@ -86,7 +86,7 @@ module Liquid
86
86
  # @liquid_syntax string | capitalize
87
87
  # @liquid_return [string]
88
88
  def capitalize(input)
89
- input.to_s.capitalize
89
+ Utils.to_s(input).capitalize
90
90
  end
91
91
 
92
92
  # @liquid_public_docs
@@ -97,7 +97,7 @@ module Liquid
97
97
  # @liquid_syntax string | escape
98
98
  # @liquid_return [string]
99
99
  def escape(input)
100
- CGI.escapeHTML(input.to_s) unless input.nil?
100
+ CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
101
101
  end
102
102
  alias_method :h, :escape
103
103
 
@@ -109,7 +109,7 @@ module Liquid
109
109
  # @liquid_syntax string | escape_once
110
110
  # @liquid_return [string]
111
111
  def escape_once(input)
112
- input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
112
+ Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
113
113
  end
114
114
 
115
115
  # @liquid_public_docs
@@ -124,7 +124,7 @@ module Liquid
124
124
  # @liquid_syntax string | url_encode
125
125
  # @liquid_return [string]
126
126
  def url_encode(input)
127
- CGI.escape(input.to_s) unless input.nil?
127
+ CGI.escape(Utils.to_s(input)) unless input.nil?
128
128
  end
129
129
 
130
130
  # @liquid_public_docs
@@ -138,7 +138,7 @@ module Liquid
138
138
  def url_decode(input)
139
139
  return if input.nil?
140
140
 
141
- result = CGI.unescape(input.to_s)
141
+ result = CGI.unescape(Utils.to_s(input))
142
142
  raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
143
143
 
144
144
  result
@@ -152,7 +152,7 @@ module Liquid
152
152
  # @liquid_syntax string | base64_encode
153
153
  # @liquid_return [string]
154
154
  def base64_encode(input)
155
- Base64.strict_encode64(input.to_s)
155
+ Base64.strict_encode64(Utils.to_s(input))
156
156
  end
157
157
 
158
158
  # @liquid_public_docs
@@ -163,7 +163,7 @@ module Liquid
163
163
  # @liquid_syntax string | base64_decode
164
164
  # @liquid_return [string]
165
165
  def base64_decode(input)
166
- input = input.to_s
166
+ input = Utils.to_s(input)
167
167
  StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
168
168
  rescue ::ArgumentError
169
169
  raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
@@ -177,7 +177,7 @@ module Liquid
177
177
  # @liquid_syntax string | base64_url_safe_encode
178
178
  # @liquid_return [string]
179
179
  def base64_url_safe_encode(input)
180
- Base64.urlsafe_encode64(input.to_s)
180
+ Base64.urlsafe_encode64(Utils.to_s(input))
181
181
  end
182
182
 
183
183
  # @liquid_public_docs
@@ -188,7 +188,7 @@ module Liquid
188
188
  # @liquid_syntax string | base64_url_safe_decode
189
189
  # @liquid_return [string]
190
190
  def base64_url_safe_decode(input)
191
- input = input.to_s
191
+ input = Utils.to_s(input)
192
192
  StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
193
193
  rescue ::ArgumentError
194
194
  raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
@@ -212,7 +212,7 @@ module Liquid
212
212
  if input.is_a?(Array)
213
213
  input.slice(offset, length) || []
214
214
  else
215
- input.to_s.slice(offset, length) || ''
215
+ Utils.to_s(input).slice(offset, length) || ''
216
216
  end
217
217
  rescue RangeError
218
218
  if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
@@ -236,10 +236,10 @@ module Liquid
236
236
  # @liquid_return [string]
237
237
  def truncate(input, length = 50, truncate_string = "...")
238
238
  return if input.nil?
239
- input_str = input.to_s
239
+ input_str = Utils.to_s(input)
240
240
  length = Utils.to_integer(length)
241
241
 
242
- truncate_string_str = truncate_string.to_s
242
+ truncate_string_str = Utils.to_s(truncate_string)
243
243
 
244
244
  l = length - truncate_string_str.length
245
245
  l = 0 if l < 0
@@ -263,7 +263,7 @@ module Liquid
263
263
  # @liquid_return [string]
264
264
  def truncatewords(input, words = 15, truncate_string = "...")
265
265
  return if input.nil?
266
- input = input.to_s
266
+ input = Utils.to_s(input)
267
267
  words = Utils.to_integer(words)
268
268
  words = 1 if words <= 0
269
269
 
@@ -277,7 +277,8 @@ module Liquid
277
277
  return input if wordlist.length <= words
278
278
 
279
279
  wordlist.pop
280
- wordlist.join(" ").concat(truncate_string.to_s)
280
+ truncate_string = Utils.to_s(truncate_string)
281
+ wordlist.join(" ").concat(truncate_string)
281
282
  end
282
283
 
283
284
  # @liquid_public_docs
@@ -288,7 +289,9 @@ module Liquid
288
289
  # @liquid_syntax string | split: string
289
290
  # @liquid_return [array[string]]
290
291
  def split(input, pattern)
291
- input.to_s.split(pattern.to_s)
292
+ pattern = Utils.to_s(pattern)
293
+ input = Utils.to_s(input)
294
+ input.split(pattern)
292
295
  end
293
296
 
294
297
  # @liquid_public_docs
@@ -299,7 +302,8 @@ module Liquid
299
302
  # @liquid_syntax string | strip
300
303
  # @liquid_return [string]
301
304
  def strip(input)
302
- input.to_s.strip
305
+ input = Utils.to_s(input)
306
+ input.strip
303
307
  end
304
308
 
305
309
  # @liquid_public_docs
@@ -310,7 +314,8 @@ module Liquid
310
314
  # @liquid_syntax string | lstrip
311
315
  # @liquid_return [string]
312
316
  def lstrip(input)
313
- input.to_s.lstrip
317
+ input = Utils.to_s(input)
318
+ input.lstrip
314
319
  end
315
320
 
316
321
  # @liquid_public_docs
@@ -321,7 +326,8 @@ module Liquid
321
326
  # @liquid_syntax string | rstrip
322
327
  # @liquid_return [string]
323
328
  def rstrip(input)
324
- input.to_s.rstrip
329
+ input = Utils.to_s(input)
330
+ input.rstrip
325
331
  end
326
332
 
327
333
  # @liquid_public_docs
@@ -332,8 +338,9 @@ module Liquid
332
338
  # @liquid_syntax string | strip_html
333
339
  # @liquid_return [string]
334
340
  def strip_html(input)
341
+ input = Utils.to_s(input)
335
342
  empty = ''
336
- result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
343
+ result = input.gsub(STRIP_HTML_BLOCKS, empty)
337
344
  result.gsub!(STRIP_HTML_TAGS, empty)
338
345
  result
339
346
  end
@@ -346,7 +353,8 @@ module Liquid
346
353
  # @liquid_syntax string | strip_newlines
347
354
  # @liquid_return [string]
348
355
  def strip_newlines(input)
349
- input.to_s.gsub(/\r?\n/, '')
356
+ input = Utils.to_s(input)
357
+ input.gsub(/\r?\n/, '')
350
358
  end
351
359
 
352
360
  # @liquid_public_docs
@@ -357,6 +365,7 @@ module Liquid
357
365
  # @liquid_syntax array | join
358
366
  # @liquid_return [string]
359
367
  def join(input, glue = ' ')
368
+ glue = Utils.to_s(glue)
360
369
  InputIterator.new(input, context).join(glue)
361
370
  end
362
371
 
@@ -378,7 +387,7 @@ module Liquid
378
387
  end
379
388
  elsif ary.all? { |el| el.respond_to?(:[]) }
380
389
  begin
381
- ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
390
+ ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
382
391
  rescue TypeError
383
392
  raise_property_error(property)
384
393
  end
@@ -407,7 +416,7 @@ module Liquid
407
416
  end
408
417
  elsif ary.all? { |el| el.respond_to?(:[]) }
409
418
  begin
410
- ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
419
+ ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
411
420
  rescue TypeError
412
421
  raise_property_error(property)
413
422
  end
@@ -424,29 +433,59 @@ module Liquid
424
433
  # @liquid_syntax array | where: string, string
425
434
  # @liquid_return [array[untyped]]
426
435
  def where(input, property, target_value = nil)
427
- ary = InputIterator.new(input, context)
436
+ filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
437
+ end
428
438
 
429
- if ary.empty?
430
- []
431
- elsif target_value.nil?
432
- ary.select do |item|
433
- item[property]
434
- rescue TypeError
435
- raise_property_error(property)
436
- rescue NoMethodError
437
- return nil unless item.respond_to?(:[])
438
- raise
439
- end
440
- else
441
- ary.select do |item|
442
- item[property] == target_value
443
- rescue TypeError
444
- raise_property_error(property)
445
- rescue NoMethodError
446
- return nil unless item.respond_to?(:[])
447
- raise
448
- end
449
- end
439
+ # @liquid_public_docs
440
+ # @liquid_type filter
441
+ # @liquid_category array
442
+ # @liquid_summary
443
+ # Filters an array to exclude items with a specific property value.
444
+ # @liquid_description
445
+ # This requires you to provide both the property name and the associated value.
446
+ # @liquid_syntax array | reject: string, string
447
+ # @liquid_return [array[untyped]]
448
+ def reject(input, property, target_value = nil)
449
+ filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
450
+ end
451
+
452
+ # @liquid_public_docs
453
+ # @liquid_type filter
454
+ # @liquid_category array
455
+ # @liquid_summary
456
+ # Tests if any item in an array has a specific property value.
457
+ # @liquid_description
458
+ # This requires you to provide both the property name and the associated value.
459
+ # @liquid_syntax array | some: string, string
460
+ # @liquid_return [boolean]
461
+ def has(input, property, target_value = nil)
462
+ filter_array(input, property, target_value) { |ary, &block| ary.any?(&block) }
463
+ end
464
+
465
+ # @liquid_public_docs
466
+ # @liquid_type filter
467
+ # @liquid_category array
468
+ # @liquid_summary
469
+ # Returns the first item in an array with a specific property value.
470
+ # @liquid_description
471
+ # This requires you to provide both the property name and the associated value.
472
+ # @liquid_syntax array | find: string, string
473
+ # @liquid_return [untyped]
474
+ def find(input, property, target_value = nil)
475
+ filter_array(input, property, target_value) { |ary, &block| ary.find(&block) }
476
+ end
477
+
478
+ # @liquid_public_docs
479
+ # @liquid_type filter
480
+ # @liquid_category array
481
+ # @liquid_summary
482
+ # Returns the index of the first item in an array with a specific property value.
483
+ # @liquid_description
484
+ # This requires you to provide both the property name and the associated value.
485
+ # @liquid_syntax array | find_index: string, string
486
+ # @liquid_return [number]
487
+ def find_index(input, property, target_value = nil)
488
+ filter_array(input, property, target_value) { |ary, &block| ary.find_index(&block) }
450
489
  end
451
490
 
452
491
  # @liquid_public_docs
@@ -465,7 +504,7 @@ module Liquid
465
504
  []
466
505
  else
467
506
  ary.uniq do |item|
468
- item[property]
507
+ fetch_property(item, property)
469
508
  rescue TypeError
470
509
  raise_property_error(property)
471
510
  rescue NoMethodError
@@ -501,7 +540,7 @@ module Liquid
501
540
  if property == "to_liquid"
502
541
  e
503
542
  elsif e.respond_to?(:[])
504
- r = e[property]
543
+ r = fetch_property(e, property)
505
544
  r.is_a?(Proc) ? r.call : r
506
545
  end
507
546
  end
@@ -525,7 +564,7 @@ module Liquid
525
564
  []
526
565
  else
527
566
  ary.reject do |item|
528
- item[property].nil?
567
+ fetch_property(item, property).nil?
529
568
  rescue TypeError
530
569
  raise_property_error(property)
531
570
  rescue NoMethodError
@@ -543,7 +582,10 @@ module Liquid
543
582
  # @liquid_syntax string | replace: string, string
544
583
  # @liquid_return [string]
545
584
  def replace(input, string, replacement = '')
546
- input.to_s.gsub(string.to_s, replacement.to_s)
585
+ string = Utils.to_s(string)
586
+ replacement = Utils.to_s(replacement)
587
+ input = Utils.to_s(input)
588
+ input.gsub(string, replacement)
547
589
  end
548
590
 
549
591
  # @liquid_public_docs
@@ -554,7 +596,10 @@ module Liquid
554
596
  # @liquid_syntax string | replace_first: string, string
555
597
  # @liquid_return [string]
556
598
  def replace_first(input, string, replacement = '')
557
- input.to_s.sub(string.to_s, replacement.to_s)
599
+ string = Utils.to_s(string)
600
+ replacement = Utils.to_s(replacement)
601
+ input = Utils.to_s(input)
602
+ input.sub(string, replacement)
558
603
  end
559
604
 
560
605
  # @liquid_public_docs
@@ -565,9 +610,9 @@ module Liquid
565
610
  # @liquid_syntax string | replace_last: string, string
566
611
  # @liquid_return [string]
567
612
  def replace_last(input, string, replacement)
568
- input = input.to_s
569
- string = string.to_s
570
- replacement = replacement.to_s
613
+ input = Utils.to_s(input)
614
+ string = Utils.to_s(string)
615
+ replacement = Utils.to_s(replacement)
571
616
 
572
617
  start_index = input.rindex(string)
573
618
 
@@ -619,7 +664,9 @@ module Liquid
619
664
  # @liquid_syntax string | append: string
620
665
  # @liquid_return [string]
621
666
  def append(input, string)
622
- input.to_s + string.to_s
667
+ input = Utils.to_s(input)
668
+ string = Utils.to_s(string)
669
+ input + string
623
670
  end
624
671
 
625
672
  # @liquid_public_docs
@@ -648,7 +695,9 @@ module Liquid
648
695
  # @liquid_syntax string | prepend: string
649
696
  # @liquid_return [string]
650
697
  def prepend(input, string)
651
- string.to_s + input.to_s
698
+ input = Utils.to_s(input)
699
+ string = Utils.to_s(string)
700
+ string + input
652
701
  end
653
702
 
654
703
  # @liquid_public_docs
@@ -659,7 +708,8 @@ module Liquid
659
708
  # @liquid_syntax string | newline_to_br
660
709
  # @liquid_return [string]
661
710
  def newline_to_br(input)
662
- input.to_s.gsub(/\r?\n/, "<br />\n")
711
+ input = Utils.to_s(input)
712
+ input.gsub(/\r?\n/, "<br />\n")
663
713
  end
664
714
 
665
715
  # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -694,11 +744,12 @@ module Liquid
694
744
  #
695
745
  # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
696
746
  def date(input, format)
697
- return input if format.to_s.empty?
747
+ str_format = Utils.to_s(format)
748
+ return input if str_format.empty?
698
749
 
699
750
  return input unless (date = Utils.to_date(input))
700
751
 
701
- date.strftime(format.to_s)
752
+ date.strftime(str_format)
702
753
  end
703
754
 
704
755
  # @liquid_public_docs
@@ -899,7 +950,7 @@ module Liquid
899
950
  if property.nil?
900
951
  item
901
952
  elsif item.respond_to?(:[])
902
- item[property]
953
+ fetch_property(item, property)
903
954
  else
904
955
  0
905
956
  end
@@ -918,6 +969,50 @@ module Liquid
918
969
 
919
970
  attr_reader :context
920
971
 
972
+ def filter_array(input, property, target_value, &block)
973
+ ary = InputIterator.new(input, context)
974
+
975
+ return [] if ary.empty?
976
+
977
+ block.call(ary) do |item|
978
+ if target_value.nil?
979
+ fetch_property(item, property)
980
+ else
981
+ fetch_property(item, property) == target_value
982
+ end
983
+ rescue TypeError
984
+ raise_property_error(property)
985
+ rescue NoMethodError
986
+ return nil unless item.respond_to?(:[])
987
+ raise
988
+ end
989
+ end
990
+
991
+ def fetch_property(drop, property_or_keys)
992
+ ##
993
+ # This keeps backward compatibility by supporting properties containing
994
+ # dots. This is valid in Liquid syntax and used in some runtimes, such as
995
+ # Shopify with metafields.
996
+ #
997
+ # Using this approach, properties like 'price.value' can be accessed in
998
+ # both of the following examples:
999
+ #
1000
+ # ```
1001
+ # [
1002
+ # { 'name' => 'Item 1', 'price.price' => 40000 },
1003
+ # { 'name' => 'Item 2', 'price' => { 'value' => 39900 } }
1004
+ # ]
1005
+ # ```
1006
+ value = drop[property_or_keys]
1007
+
1008
+ return value if !value.nil? || !property_or_keys.is_a?(String)
1009
+
1010
+ keys = property_or_keys.split('.')
1011
+ keys.reduce(drop) do |drop, key|
1012
+ drop.respond_to?(:[]) ? drop[key] : drop
1013
+ end
1014
+ end
1015
+
921
1016
  def raise_property_error(property)
922
1017
  raise Liquid::ArgumentError, "cannot select the property '#{property}'"
923
1018
  end
@@ -968,7 +1063,18 @@ module Liquid
968
1063
  end
969
1064
 
970
1065
  def join(glue)
971
- to_a.join(glue.to_s)
1066
+ first = true
1067
+ output = +""
1068
+ each do |item|
1069
+ if first
1070
+ first = false
1071
+ else
1072
+ output << glue
1073
+ end
1074
+
1075
+ output << Liquid::Utils.to_s(item)
1076
+ end
1077
+ output
972
1078
  end
973
1079
 
974
1080
  def concat(args)
@@ -68,7 +68,13 @@ module Liquid
68
68
  def variables_from_string(markup)
69
69
  markup.split(',').collect do |var|
70
70
  var =~ /\s*(#{QuotedFragment})\s*/o
71
- Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
71
+ next unless Regexp.last_match(1)
72
+
73
+ # Expression Parser returns cached objects, and we need to dup them to
74
+ # start the cycle over for each new cycle call.
75
+ # Liquid-C does not have a cache, so we don't need to dup the object.
76
+ var = parse_expression(Regexp.last_match(1))
77
+ var.is_a?(VariableLookup) ? var.dup : var
72
78
  end.compact
73
79
  end
74
80
 
@@ -88,7 +88,7 @@ module Liquid
88
88
  end
89
89
 
90
90
  def strict_parse(markup)
91
- p = Parser.new(markup)
91
+ p = @parse_context.new_parser(markup)
92
92
  @variable_name = p.consume(:id)
93
93
  raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
94
94
 
@@ -102,7 +102,7 @@ module Liquid
102
102
  end
103
103
 
104
104
  def strict_parse(markup)
105
- p = Parser.new(markup)
105
+ p = @parse_context.new_parser(markup)
106
106
  condition = parse_binary_comparisons(p)
107
107
  p.consume(:end_of_string)
108
108
  condition
@@ -1,20 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+
3
5
  module Liquid
4
6
  class Tokenizer
5
7
  attr_reader :line_number, :for_liquid_tag
6
8
 
7
- def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
8
- @source = source.to_s.to_str
9
- @line_number = line_number || (line_numbers ? 1 : nil)
9
+ TAG_END = /%\}/
10
+ TAG_OR_VARIABLE_START = /\{[\{\%]/
11
+ NEWLINE = /\n/
12
+
13
+ OPEN_CURLEY = "{".ord
14
+ CLOSE_CURLEY = "}".ord
15
+ PERCENTAGE = "%".ord
16
+
17
+ def initialize(
18
+ source:,
19
+ string_scanner:,
20
+ line_numbers: false,
21
+ line_number: nil,
22
+ for_liquid_tag: false
23
+ )
24
+ @line_number = line_number || (line_numbers ? 1 : nil)
10
25
  @for_liquid_tag = for_liquid_tag
11
- @offset = 0
12
- @tokens = tokenize
26
+ @source = source.to_s.to_str
27
+ @offset = 0
28
+ @tokens = []
29
+
30
+ if @source
31
+ @ss = string_scanner
32
+ @ss.string = @source
33
+ tokenize
34
+ end
13
35
  end
14
36
 
15
37
  def shift
16
38
  token = @tokens[@offset]
17
- return nil unless token
39
+
40
+ return unless token
18
41
 
19
42
  @offset += 1
20
43
 
@@ -28,18 +51,105 @@ module Liquid
28
51
  private
29
52
 
30
53
  def tokenize
31
- return [] if @source.empty?
54
+ if @for_liquid_tag
55
+ @tokens = @source.split("\n")
56
+ else
57
+ @tokens << shift_normal until @ss.eos?
58
+ end
32
59
 
33
- return @source.split("\n") if @for_liquid_tag
60
+ @source = nil
61
+ @ss = nil
62
+ end
34
63
 
35
- tokens = @source.split(TemplateParser)
64
+ def shift_normal
65
+ token = next_token
36
66
 
37
- # removes the rogue empty element at the beginning of the array
38
- if tokens[0]&.empty?
39
- @offset += 1
67
+ return unless token
68
+
69
+ token
70
+ end
71
+
72
+ def next_token
73
+ # possible states: :text, :tag, :variable
74
+ byte_a = @ss.peek_byte
75
+
76
+ if byte_a == OPEN_CURLEY
77
+ @ss.scan_byte
78
+
79
+ byte_b = @ss.peek_byte
80
+
81
+ if byte_b == PERCENTAGE
82
+ @ss.scan_byte
83
+ return next_tag_token
84
+ elsif byte_b == OPEN_CURLEY
85
+ @ss.scan_byte
86
+ return next_variable_token
87
+ end
88
+
89
+ @ss.pos -= 1
40
90
  end
41
91
 
42
- tokens
92
+ next_text_token
93
+ end
94
+
95
+ def next_text_token
96
+ start = @ss.pos
97
+
98
+ unless @ss.skip_until(TAG_OR_VARIABLE_START)
99
+ token = @ss.rest
100
+ @ss.terminate
101
+ return token
102
+ end
103
+
104
+ pos = @ss.pos -= 2
105
+ @source.byteslice(start, pos - start)
106
+ end
107
+
108
+ def next_variable_token
109
+ start = @ss.pos - 2
110
+
111
+ byte_a = byte_b = @ss.scan_byte
112
+
113
+ while byte_b
114
+ byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
115
+
116
+ break unless byte_a
117
+
118
+ if @ss.eos?
119
+ return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
120
+ end
121
+
122
+ byte_b = @ss.scan_byte
123
+
124
+ if byte_a == CLOSE_CURLEY
125
+ if byte_b == CLOSE_CURLEY
126
+ return @source.byteslice(start, @ss.pos - start)
127
+ elsif byte_b != CLOSE_CURLEY
128
+ @ss.pos -= 1
129
+ return @source.byteslice(start, @ss.pos - start)
130
+ end
131
+ elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
132
+ return next_tag_token_with_start(start)
133
+ end
134
+
135
+ byte_a = byte_b
136
+ end
137
+
138
+ "{{"
139
+ end
140
+
141
+ def next_tag_token
142
+ start = @ss.pos - 2
143
+ if (len = @ss.skip_until(TAG_END))
144
+ @source.byteslice(start, len + 2)
145
+ else
146
+ "{%"
147
+ end
148
+ end
149
+
150
+ def next_tag_token_with_start(start)
151
+ @ss.skip_until(TAG_END)
152
+ @source.byteslice(start, @ss.pos - start)
43
153
  end
44
154
  end
45
155
  end