liquid 5.6.0 → 5.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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