luoma 0.1.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.
Files changed (152) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.rdoc_options +16 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +68 -0
  7. data/Rakefile +69 -0
  8. data/Steepfile +41 -0
  9. data/certs/jgrp.pem +27 -0
  10. data/docs/configuration.md +180 -0
  11. data/docs/custom_filters.md +3 -0
  12. data/docs/custom_tags.md +3 -0
  13. data/docs/expressions.md +3 -0
  14. data/docs/extension_types.md +41 -0
  15. data/docs/filter_reference.md +1702 -0
  16. data/docs/index.md +73 -0
  17. data/docs/luoma_for_template_authors.md +3 -0
  18. data/docs/markdown.md +111 -0
  19. data/docs/predicate_reference.md +3 -0
  20. data/docs/static_analysis.md +3 -0
  21. data/docs/tag_reference.md +352 -0
  22. data/docs/template_loaders.md +177 -0
  23. data/docs/undefined_variables.md +3 -0
  24. data/docs-requirements.txt +11 -0
  25. data/docs_/cycle.md +17 -0
  26. data/docs_/font_rendering_example.md +157 -0
  27. data/docs_/header_block_example.md +51 -0
  28. data/docs_/increment_and_decrement.md +49 -0
  29. data/docs_/logo_style_example.md +40 -0
  30. data/docs_/migration.md +11 -0
  31. data/docs_/notes.md +19 -0
  32. data/lib/luoma/cache.rb +80 -0
  33. data/lib/luoma/chain_hash.rb +54 -0
  34. data/lib/luoma/context.rb +227 -0
  35. data/lib/luoma/drop.rb +84 -0
  36. data/lib/luoma/drops/block.rb +33 -0
  37. data/lib/luoma/drops/expression.rb +29 -0
  38. data/lib/luoma/drops/html_safe.rb +36 -0
  39. data/lib/luoma/drops/range.rb +69 -0
  40. data/lib/luoma/drops/undefined.rb +119 -0
  41. data/lib/luoma/environment.rb +479 -0
  42. data/lib/luoma/errors.rb +79 -0
  43. data/lib/luoma/escape.rb +35 -0
  44. data/lib/luoma/expression.rb +1271 -0
  45. data/lib/luoma/filter.rb +97 -0
  46. data/lib/luoma/filters/array.rb +372 -0
  47. data/lib/luoma/filters/date.rb +19 -0
  48. data/lib/luoma/filters/default.rb +16 -0
  49. data/lib/luoma/filters/json.rb +14 -0
  50. data/lib/luoma/filters/math.rb +84 -0
  51. data/lib/luoma/filters/size.rb +13 -0
  52. data/lib/luoma/filters/slice.rb +52 -0
  53. data/lib/luoma/filters/sort.rb +113 -0
  54. data/lib/luoma/filters/string.rb +186 -0
  55. data/lib/luoma/fnv.rb +15 -0
  56. data/lib/luoma/lexer.rb +106 -0
  57. data/lib/luoma/lexer_legacy.rb +396 -0
  58. data/lib/luoma/lexer_unified.rb +279 -0
  59. data/lib/luoma/loader.rb +50 -0
  60. data/lib/luoma/loaders/choice_loader.rb +20 -0
  61. data/lib/luoma/loaders/file_system_loader.rb +75 -0
  62. data/lib/luoma/loaders/mixins.rb +52 -0
  63. data/lib/luoma/markup.rb +109 -0
  64. data/lib/luoma/parser.rb +252 -0
  65. data/lib/luoma/parser_unified.rb +979 -0
  66. data/lib/luoma/predicates/blank.rb +19 -0
  67. data/lib/luoma/predicates/defined.rb +10 -0
  68. data/lib/luoma/predicates/empty.rb +15 -0
  69. data/lib/luoma/predicates/type.rb +35 -0
  70. data/lib/luoma/static_analysis.rb +427 -0
  71. data/lib/luoma/tags/assign.rb +63 -0
  72. data/lib/luoma/tags/break.rb +23 -0
  73. data/lib/luoma/tags/capture.rb +43 -0
  74. data/lib/luoma/tags/case.rb +137 -0
  75. data/lib/luoma/tags/comment.rb +17 -0
  76. data/lib/luoma/tags/continue.rb +23 -0
  77. data/lib/luoma/tags/define.rb +42 -0
  78. data/lib/luoma/tags/else.rb +30 -0
  79. data/lib/luoma/tags/for.rb +108 -0
  80. data/lib/luoma/tags/if.rb +109 -0
  81. data/lib/luoma/tags/import.rb +91 -0
  82. data/lib/luoma/tags/include.rb +77 -0
  83. data/lib/luoma/tags/output.rb +20 -0
  84. data/lib/luoma/tags/raw.rb +26 -0
  85. data/lib/luoma/tags/render.rb +92 -0
  86. data/lib/luoma/tags/with.rb +55 -0
  87. data/lib/luoma/template.rb +178 -0
  88. data/lib/luoma/token.rb +84 -0
  89. data/lib/luoma/version.rb +5 -0
  90. data/lib/luoma.rb +75 -0
  91. data/sig/luoma/cache.rbs +44 -0
  92. data/sig/luoma/chain_hash.rbs +26 -0
  93. data/sig/luoma/context.rbs +115 -0
  94. data/sig/luoma/drop.rbs +44 -0
  95. data/sig/luoma/drops/block.rbs +10 -0
  96. data/sig/luoma/drops/expression.rbs +10 -0
  97. data/sig/luoma/drops/html_safe.rbs +14 -0
  98. data/sig/luoma/drops/range.rbs +15 -0
  99. data/sig/luoma/drops/undefined.rbs +27 -0
  100. data/sig/luoma/environment.rbs +212 -0
  101. data/sig/luoma/errors.rbs +67 -0
  102. data/sig/luoma/escape.rbs +15 -0
  103. data/sig/luoma/expression.rbs +514 -0
  104. data/sig/luoma/filter.rbs +66 -0
  105. data/sig/luoma/filters/array.rbs +74 -0
  106. data/sig/luoma/filters/date.rbs +9 -0
  107. data/sig/luoma/filters/default.rbs +8 -0
  108. data/sig/luoma/filters/json.rbs +7 -0
  109. data/sig/luoma/filters/math.rbs +37 -0
  110. data/sig/luoma/filters/size.rbs +6 -0
  111. data/sig/luoma/filters/slice.rbs +10 -0
  112. data/sig/luoma/filters/sort.rbs +17 -0
  113. data/sig/luoma/filters/string.rbs +103 -0
  114. data/sig/luoma/fnv.rbs +3 -0
  115. data/sig/luoma/lexer.rbs +42 -0
  116. data/sig/luoma/lexer_legacy.rbs +81 -0
  117. data/sig/luoma/lexer_unified.rbs +49 -0
  118. data/sig/luoma/loader.rbs +40 -0
  119. data/sig/luoma/loaders/choice_loader.rbs +9 -0
  120. data/sig/luoma/loaders/file_system_loader.rbs +19 -0
  121. data/sig/luoma/loaders/mixins.rbs +16 -0
  122. data/sig/luoma/markup.rbs +68 -0
  123. data/sig/luoma/parser.rbs +105 -0
  124. data/sig/luoma/parser_unified.rbs +150 -0
  125. data/sig/luoma/predicates/blank.rbs +6 -0
  126. data/sig/luoma/predicates/defined.rbs +6 -0
  127. data/sig/luoma/predicates/empty.rbs +6 -0
  128. data/sig/luoma/predicates/type.rbs +21 -0
  129. data/sig/luoma/static_analysis.rbs +141 -0
  130. data/sig/luoma/tags/assign.rbs +12 -0
  131. data/sig/luoma/tags/break.rbs +11 -0
  132. data/sig/luoma/tags/capture.rbs +14 -0
  133. data/sig/luoma/tags/case.rbs +36 -0
  134. data/sig/luoma/tags/comment.rbs +11 -0
  135. data/sig/luoma/tags/continue.rbs +11 -0
  136. data/sig/luoma/tags/define.rbs +16 -0
  137. data/sig/luoma/tags/else.rbs +17 -0
  138. data/sig/luoma/tags/for.rbs +26 -0
  139. data/sig/luoma/tags/if.rbs +45 -0
  140. data/sig/luoma/tags/import.rbs +13 -0
  141. data/sig/luoma/tags/include.rbs +13 -0
  142. data/sig/luoma/tags/output.rbs +13 -0
  143. data/sig/luoma/tags/raw.rbs +11 -0
  144. data/sig/luoma/tags/render.rbs +13 -0
  145. data/sig/luoma/tags/with.rbs +15 -0
  146. data/sig/luoma/template.rbs +117 -0
  147. data/sig/luoma/token.rbs +81 -0
  148. data/sig/luoma.rbs +11 -0
  149. data/zensical.toml +363 -0
  150. data.tar.gz.sig +0 -0
  151. metadata +233 -0
  152. metadata.gz.sig +0 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ module Filters
5
+ INFINITY_ARRAY = [Float::INFINITY].freeze # : [Float]
6
+
7
+ # Return a sorted array of items from _left_. If _left_ contains
8
+ # incomparable items, return _left_ as an array with its original
9
+ # ordering.
10
+ #
11
+ # Coerce _left_ to an array if it's not one already.
12
+ def self.sort(context, left, key = nil)
13
+ left = context.to_a(left)
14
+
15
+ # @type var sort_: ^(Array[untyped]) ?{ (untyped, Integer) -> untyped } -> Array[untyped]
16
+ sort_ = lambda do |array, &key|
17
+ decorated = array.each_with_index.map do |item, index|
18
+ [item, key ? key.call(item, index) : item]
19
+ end
20
+
21
+ begin
22
+ decorated.sort! do |(_, a), (_, b)|
23
+ cmp = context.cmp(a, b)
24
+
25
+ case cmp
26
+ when nil
27
+ if a.nil? || context.nothing?(a)
28
+ 1
29
+ elsif b.nil? || context.nothing?(b)
30
+ -1
31
+ else
32
+ raise IncomparableValuesError
33
+ end
34
+ else
35
+ cmp
36
+ end
37
+ end
38
+ rescue IncomparableValuesError
39
+ raise LuomaError.new("Cannot sort incomparable values") if context.render_context.env.strict
40
+
41
+ return array # rubocop:disable Lint/NoReturnInBeginEndBlocks
42
+ end
43
+
44
+ decorated.map!(&:first)
45
+ end
46
+
47
+ if key.nil? || context.nothing?(key)
48
+ sort_.call(left)
49
+ elsif key.is_a?(ExpressionDrop)
50
+ sort_.call(left) { |item, index| key.expr.call_with_index(item, index) }
51
+ else
52
+ key = context.to_string(key)
53
+ sort_.call(left) { |item, _index| context.fetch(item, key) }
54
+ end
55
+ end
56
+
57
+ def self.sort_natural(context, left, key = :nothing)
58
+ left = context.to_enumerable(left)
59
+
60
+ if key.nil? || context.nothing?(key)
61
+ left.sort { |a, b| nil_safe_casecmp(a, b) }
62
+ elsif key.is_a?(ExpressionDrop)
63
+ key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
64
+ nil_safe_casecmp(a.first, b.first)
65
+ end.map(&:last)
66
+ else
67
+ key = context.to_string(key)
68
+ left.sort { |a, b| nil_safe_casecmp(context.fetch(a, key), context.fetch(b, key)) }
69
+ end
70
+ end
71
+
72
+ def self.sort_numeric(context, left, key = :nothing)
73
+ left = context.to_enumerable(left)
74
+
75
+ if key.nil? || context.nothing?(key)
76
+ left.sort { |a, b| numeric_compare(a, b, context) }
77
+ elsif key.is_a?(ExpressionDrop)
78
+ key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
79
+ numeric_compare(a.first, b.first, context)
80
+ end.map(&:last)
81
+ else
82
+ key = context.to_string(key)
83
+ left.sort { |a, b| numeric_compare(context.fetch(a, key), context.fetch(b, key), context) }
84
+ end
85
+ end
86
+
87
+ def self.nil_safe_casecmp(left, right)
88
+ if !left.nil? && !right.nil?
89
+ left.to_s.casecmp(right.to_s)
90
+ elsif left.nil? && right.nil?
91
+ 0
92
+ else
93
+ left.nil? ? 1 : -1
94
+ end
95
+ end
96
+
97
+ def self.numeric_compare(left, right, context)
98
+ res = ints(left, context) <=> ints(right, context)
99
+ res || -1
100
+ end
101
+
102
+ def self.ints(obj, context)
103
+ if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal)
104
+ [obj]
105
+ else
106
+ numeric = context.to_string(obj).scan(/(?<=\.)0+|-?\d+/)
107
+ return INFINITY_ARRAY if numeric.nil? || numeric.empty?
108
+
109
+ numeric.map(&:to_i) # steep:ignore
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+
5
+ module Luoma
6
+ module Filters
7
+ # Return _left_ concatenated with _right_.
8
+ # Coerce _left_ and _right_ to strings if they aren't strings already.
9
+ def self.append(context, left, right)
10
+ context.to_string(left) + context.to_string(right)
11
+ end
12
+
13
+ # Return _left_ with the first character in uppercase and the rest lowercase.
14
+ # Coerce _left_ to a string if it is not one already.
15
+ def self.capitalize(context, left)
16
+ context.to_string(left).capitalize
17
+ end
18
+
19
+ # Return _left_ with all characters converted to lowercase.
20
+ # Coerce _left_ to a string if it is not one already.
21
+ def self.downcase(context, left)
22
+ context.to_string(left).downcase
23
+ end
24
+
25
+ # Return _left_ with all characters converted to uppercase.
26
+ # Coerce _left_ to a string if it is not one already.
27
+ def self.upcase(context, left)
28
+ context.to_string(left).upcase
29
+ end
30
+
31
+ # Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
32
+ # Coerce _left_ to a string if it is not one already.
33
+ def self.escape(context, left)
34
+ CGI.escape_html(context.to_string(left)) unless left.nil?
35
+ end
36
+
37
+ # Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
38
+ # Coerce _left_ to a string if it is not one already.
39
+ #
40
+ # It is safe to use `escape_once` on string values that already contain HTML-escape sequences.
41
+ def self.escape_once(context, left)
42
+ CGI.escape_html(CGI.unescape_html(context.to_string(left)))
43
+ end
44
+
45
+ # Return _left_ with leading whitespace removed.
46
+ # Coerce _left_ to a string if it is not one already.
47
+ def self.lstrip(context, left)
48
+ context.to_string(left).lstrip
49
+ end
50
+
51
+ # Return _left_ with trailing whitespace removed.
52
+ # Coerce _left_ to a string if it is not one already.
53
+ def self.rstrip(context, left)
54
+ context.to_string(left).rstrip
55
+ end
56
+
57
+ # Return _left_ with leading and trailing whitespace removed.
58
+ # Coerce _left_ to a string if it is not one already.
59
+ def self.strip(context, left)
60
+ context.to_string(left).strip
61
+ end
62
+
63
+ # Return _left_ with LF or CRLF replaced with `<br />\n`.
64
+ def self.newline_to_br(context, left)
65
+ context.to_string(left).gsub(/\r?\n/, "<br />\n")
66
+ end
67
+
68
+ # Return _right_ concatenated with _left_.
69
+ # Coerce _left_ and _right_ to strings if they aren't strings already.
70
+ def self.prepend_(context, left, right)
71
+ context.to_string(right) + context.to_string(left)
72
+ end
73
+
74
+ # Return _left_ with all occurrences of _pattern_ replaced with _replacement_.
75
+ # All arguments are coerced to strings if they aren't strings already.
76
+ def self.replace(context, left, pattern, replacement = "")
77
+ context.to_string(left).gsub(context.to_string(pattern), context.to_string(replacement))
78
+ end
79
+
80
+ # Return _left_ with the first occurrence of _pattern_ replaced with _replacement_.
81
+ # All arguments are coerced to strings if they aren't strings already.
82
+ def self.replace_first(context, left, pattern, replacement = "")
83
+ context.to_string(left).sub(context.to_string(pattern), context.to_string(replacement))
84
+ end
85
+
86
+ # Return _left_ with the last occurrence of _pattern_ replaced with _replacement_.
87
+ # All arguments are coerced to strings if they aren't strings already.
88
+ def self.replace_last(context, left, pattern, replacement)
89
+ return left + replacement if context.nothing?(pattern)
90
+
91
+ head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
92
+ return left if match.empty?
93
+
94
+ head + context.to_string(replacement) + tail
95
+ end
96
+
97
+ # Return _left_ with all occurrences of _pattern_ removed.
98
+ # All arguments are coerced to strings if they aren't strings already.
99
+ def self.remove(context, left, pattern)
100
+ context.to_string(left).gsub(context.to_string(pattern), context.to_string(""))
101
+ end
102
+
103
+ # Return _left_ with the first occurrence of _pattern_ removed.
104
+ # All arguments are coerced to strings if they aren't strings already.
105
+ def self.remove_first(context, left, pattern)
106
+ context.to_string(left).sub(context.to_string(pattern), context.to_string(""))
107
+ end
108
+
109
+ # Return _left_ with the last occurrence of _pattern_ removed.
110
+ # All arguments are coerced to strings if they aren't strings already.
111
+ def self.remove_last(context, left, pattern)
112
+ return left if context.nothing?(pattern)
113
+
114
+ head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
115
+ return left if match.empty?
116
+
117
+ head + tail
118
+ end
119
+
120
+ # Split _left_ on every occurrence of _pattern_.
121
+ def self.split(context, left, pattern)
122
+ context.to_string(left).split(context.to_string(pattern))
123
+ end
124
+
125
+ RE_HTML_BLOCKS = Regexp.union(
126
+ %r{<script.*?</script>}m,
127
+ /<!--.*?-->/m,
128
+ %r{<style.*?</style>}m
129
+ )
130
+
131
+ RE_HTML_TAGS = /<.*?>/m
132
+
133
+ # Return _left_ with HTML tags removed.
134
+ def self.strip_html(context, left)
135
+ context.to_string(left).gsub(RE_HTML_BLOCKS, "").gsub(RE_HTML_TAGS, "")
136
+ end
137
+
138
+ # Return _left_ with CR and LF removed.
139
+ def self.strip_newlines(context, left)
140
+ context.to_string(left).gsub(/\r?\n/, "")
141
+ end
142
+
143
+ def self.truncate(context, left, max_length = 50, ellipsis = "...")
144
+ return if left.nil? || context.nothing?(left)
145
+
146
+ left = context.to_string(left)
147
+ max_length = context.to_i(max_length)
148
+ return left if left.length <= max_length
149
+
150
+ ellipsis = context.to_string(ellipsis)
151
+ return ellipsis[0, max_length] if ellipsis.length >= max_length
152
+
153
+ "#{left[0...(max_length - ellipsis.length)]}#{ellipsis}"
154
+ end
155
+
156
+ def self.truncatewords(context, left, max_words = 15, ellipsis = "...")
157
+ return if left.nil? || context.nothing?(left)
158
+
159
+ left = context.to_string(left)
160
+ max_words = context.to_i(max_words).clamp(1, 10_000)
161
+ words = left.split(" ", max_words + 1)
162
+ return left if words.length <= max_words
163
+
164
+ ellipsis = context.to_string(ellipsis)
165
+ words.pop
166
+ "#{words.join(" ")}#{ellipsis}"
167
+ end
168
+
169
+ def self.url_encode(context, left)
170
+ CGI.escape(context.to_string(left)) unless left.nil? || context.nothing?(left)
171
+ end
172
+
173
+ def self.url_decode(context, left)
174
+ return if left.nil? || context.nothing?(left)
175
+
176
+ decoded = CGI.unescape(context.to_string(left))
177
+ raise context.argument_error("invalid byte sequence") unless decoded.valid_encoding?
178
+
179
+ decoded
180
+ end
181
+
182
+ def self.squish(context, left)
183
+ context.to_string(left).strip.gsub(/\s+/, " ") unless left.nil?
184
+ end
185
+ end
186
+ end
data/lib/luoma/fnv.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ def self.fnv1a32(s)
5
+ hash = 0x811c9dc5 # offset basis for 32 bit hash
6
+
7
+ s.bytes do |b|
8
+ hash ^= b
9
+ hash *= 0x01000193 # FNV prime for 32 bit hash
10
+ hash &= (1 << 32) - 1
11
+ end
12
+
13
+ hash
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Luoma
6
+ # The base class for template source code tokenizers.
7
+ class BaseLexer
8
+ attr_reader :tokens
9
+
10
+ #: (Environment, String) -> Array[t_token]
11
+ def self.tokenize(env, source)
12
+ lexer = new(env, source)
13
+ lexer.go
14
+ lexer.tokens
15
+ end
16
+
17
+ #: (Environment, String) -> void
18
+ def initialize(env, source)
19
+ @env = env
20
+ @source = source
21
+ @scanner = StringScanner.new(source)
22
+ @start = 0
23
+ @tokens = [] #: Array[t_token]
24
+ end
25
+
26
+ #: () -> void
27
+ def go
28
+ state = :scan_markup
29
+ state = send(state) until state.nil?
30
+ end
31
+
32
+ protected
33
+
34
+ # Scanner entry point. If a symbol is returned it should match the name of
35
+ # a method implementing the next state. A return value of `nil` means we
36
+ # should stop scanning.
37
+ #: () -> Symbol?
38
+ def scan_markup
39
+ raise "not implemented"
40
+ end
41
+
42
+ # Emit a token of `kind` spanning @start to @scanner.pos.
43
+ #: (t_token_kind) -> void
44
+ def emit(kind)
45
+ # @tokens << { kind: kind, start: @start, stop: @scanner.pos - 1 }
46
+ @tokens << [kind, @start, @scanner.pos - 1]
47
+ @start = @scanner.pos
48
+ end
49
+
50
+ # Return the start index of the next match of `pattern` without advancing
51
+ # the scanner.
52
+ #
53
+ # It's faster to use `@scanner.string.byteindex(s, @scanner.pos)` if you
54
+ # are searching for a string literal instead of a pattern.
55
+ #
56
+ #: (Regexp) -> Integer?
57
+ def index(pattern)
58
+ byte_offset = @scanner.exist?(pattern)
59
+ return nil unless byte_offset
60
+
61
+ @scanner.pos + byte_offset - (@scanner.matched_size || raise)
62
+ end
63
+
64
+ #: (Regexp) -> String?
65
+ def scan(pattern)
66
+ # NOTE: calling @scanner.scan directly yields a significant increase in
67
+ # performance
68
+ @scanner.scan(pattern)
69
+ end
70
+
71
+ # Scan up to but not including `pattern`.
72
+ #: (Regexp) -> bool
73
+ def scan_until?(pattern)
74
+ byte_offset = @scanner.exist?(pattern)
75
+ if byte_offset.nil?
76
+ false
77
+ else
78
+ @scanner.pos += byte_offset - (@scanner.matched_size || raise)
79
+ true
80
+ end
81
+ end
82
+
83
+ #: (Regexp) -> bool
84
+ def skip?(pattern)
85
+ if @scanner.scan(pattern)
86
+ @start = @scanner.pos
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ # Skip up to but not including `pattern`.
94
+ #: (Regexp) -> bool
95
+ def skip_until?(pattern)
96
+ byte_offset = @scanner.exist?(pattern)
97
+ if byte_offset.nil?
98
+ false
99
+ else
100
+ @scanner.pos += byte_offset - (@scanner.matched_size || raise)
101
+ @start = @scanner.pos
102
+ true
103
+ end
104
+ end
105
+ end
106
+ end