mustermann 0.0.1
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.test_queue_stats +0 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +644 -0
- data/Rakefile +3 -0
- data/bench/capturing.rb +24 -0
- data/bench/simple_vs_sinatra.rb +23 -0
- data/bench/template_vs_addressable.rb +23 -0
- data/lib/mustermann.rb +40 -0
- data/lib/mustermann/ast.rb +403 -0
- data/lib/mustermann/error.rb +14 -0
- data/lib/mustermann/extension.rb +52 -0
- data/lib/mustermann/identity.rb +19 -0
- data/lib/mustermann/pattern.rb +142 -0
- data/lib/mustermann/rails.rb +26 -0
- data/lib/mustermann/regexp_based.rb +30 -0
- data/lib/mustermann/shell.rb +22 -0
- data/lib/mustermann/simple.rb +35 -0
- data/lib/mustermann/simple_match.rb +30 -0
- data/lib/mustermann/sinatra.rb +32 -0
- data/lib/mustermann/template.rb +140 -0
- data/lib/mustermann/version.rb +3 -0
- data/mustermann.gemspec +28 -0
- data/spec/ast_spec.rb +8 -0
- data/spec/extension_spec.rb +153 -0
- data/spec/identity_spec.rb +82 -0
- data/spec/mustermann_spec.rb +0 -0
- data/spec/pattern_spec.rb +16 -0
- data/spec/rails_spec.rb +453 -0
- data/spec/regexp_based_spec.rb +8 -0
- data/spec/shell_spec.rb +108 -0
- data/spec/simple_match_spec.rb +10 -0
- data/spec/simple_spec.rb +236 -0
- data/spec/sinatra_spec.rb +554 -0
- data/spec/support.rb +78 -0
- data/spec/template_spec.rb +814 -0
- metadata +227 -0
data/Rakefile
ADDED
data/bench/capturing.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann'
|
5
|
+
require 'addressable/template'
|
6
|
+
|
7
|
+
list = [
|
8
|
+
/\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z/,
|
9
|
+
Mustermann.new('/*/:name', type: :sinatra),
|
10
|
+
Mustermann.new('/*/:name', type: :simple),
|
11
|
+
Mustermann.new('/*prefix/:name', type: :rails),
|
12
|
+
Mustermann.new('{/prefix*}/{name}', type: :template),
|
13
|
+
#Addressable::Template.new('{/prefix*}/{name}')
|
14
|
+
]
|
15
|
+
|
16
|
+
string = '/a/b/c/d'
|
17
|
+
|
18
|
+
Benchmark.bmbm do |x|
|
19
|
+
list.each do |pattern|
|
20
|
+
x.report pattern.class.to_s do
|
21
|
+
100_000.times { pattern.match(string).captures }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/simple'
|
5
|
+
require 'mustermann/sinatra'
|
6
|
+
|
7
|
+
[Mustermann::Simple, Mustermann::Sinatra].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/:name")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
splat = klass.new("/*")
|
19
|
+
x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } }
|
20
|
+
x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } }
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/template'
|
5
|
+
require 'addressable/template'
|
6
|
+
|
7
|
+
[Mustermann::Template, Addressable::Template].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/{match}")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
explode = klass.new("{/segments*}")
|
19
|
+
x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } }
|
20
|
+
x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } }
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
data/lib/mustermann.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Namespace and main entry point for the Mustermann library.
|
2
|
+
#
|
3
|
+
# Under normal circumstences the only external API entry point you should be using is {Mustermann.new}.
|
4
|
+
module Mustermann
|
5
|
+
# @param [String] string The string represenation of the new pattern
|
6
|
+
# @param [Hash] options The options hash
|
7
|
+
# @return [Mustermann::Pattern] pattern corresponding to string.
|
8
|
+
# @see file:README.md#Types_and_Options "Types and Options" in the README
|
9
|
+
def self.new(string, type: :sinatra, **options)
|
10
|
+
options.any? ? self[type].new(string, **options) : self[type].new(string)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @!visibility private
|
14
|
+
def self.[](key)
|
15
|
+
constant, library = register.fetch(key)
|
16
|
+
require library if library
|
17
|
+
constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!visibility private
|
21
|
+
def self.register(identifier = nil, constant = identifier.to_s.capitalize, load: "mustermann/#{identifier}")
|
22
|
+
@register ||= {}
|
23
|
+
@register[identifier] = [constant, load] if identifier
|
24
|
+
@register
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def self.extend_object(object)
|
29
|
+
return super unless defined? ::Sinatra::Base and object < ::Sinatra::Base
|
30
|
+
require 'mustermann/extension'
|
31
|
+
object.register Extension
|
32
|
+
end
|
33
|
+
|
34
|
+
register :identity
|
35
|
+
register :rails
|
36
|
+
register :shell
|
37
|
+
register :simple
|
38
|
+
register :sinatra
|
39
|
+
register :template
|
40
|
+
end
|
@@ -0,0 +1,403 @@
|
|
1
|
+
require 'mustermann/regexp_based'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Superclass for pattern styles that parse an AST from the string pattern.
|
6
|
+
# @abstract
|
7
|
+
class AST < RegexpBased
|
8
|
+
supported_options :capture, :except, :greedy, :space_matches_plus
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
class Node
|
12
|
+
# @!visibility private
|
13
|
+
attr_accessor :payload
|
14
|
+
|
15
|
+
# Helper for creating a new instance and calling #parse on it.
|
16
|
+
# @return [Node]
|
17
|
+
# @!visibility private
|
18
|
+
def self.parse(element = new, &block)
|
19
|
+
element.tap { |n| n.parse(&block) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
def initialize(payload = nil, **options)
|
24
|
+
options.each { |key, value| public_send("#{key}=", value) }
|
25
|
+
self.payload = payload
|
26
|
+
end
|
27
|
+
|
28
|
+
# Double dispatch helper for reading from the buffer into the payload.
|
29
|
+
# @!visibility private
|
30
|
+
def parse
|
31
|
+
self.payload ||= []
|
32
|
+
while element = yield
|
33
|
+
payload << element
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] regular expression corresponding to node
|
38
|
+
# @!visibility private
|
39
|
+
def compile(options)
|
40
|
+
Array(payload).map { |e| e.compile(options) }.join
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Node] This node after tree transformation. Might be self.
|
44
|
+
# @!visibility private
|
45
|
+
def transform
|
46
|
+
self.payload = payload.transform if payload.respond_to? :transform
|
47
|
+
return self unless Array === payload
|
48
|
+
|
49
|
+
new_payload = []
|
50
|
+
with_lookahead = []
|
51
|
+
|
52
|
+
payload.each do |element|
|
53
|
+
element = element.transform
|
54
|
+
if with_lookahead.empty?
|
55
|
+
list = element.expect_lookahead? ? with_lookahead : new_payload
|
56
|
+
list << element
|
57
|
+
elsif element.lookahead?
|
58
|
+
with_lookahead << element
|
59
|
+
else
|
60
|
+
with_lookahead = [WithLookAhead.new(with_lookahead, false)] if element.separator? and with_lookahead.size > 1
|
61
|
+
new_payload.concat(with_lookahead)
|
62
|
+
new_payload << element
|
63
|
+
with_lookahead.clear
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
with_lookahead = [WithLookAhead.new(with_lookahead, true)] if with_lookahead.size > 1
|
68
|
+
new_payload.concat(with_lookahead)
|
69
|
+
@payload = new_payload
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# @!visibility private
|
74
|
+
def separator?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
# @!visibility private
|
79
|
+
def lookahead?(in_lookahead = false)
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# @!visibility private
|
84
|
+
def expect_lookahead?
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [String] Regular expression for matching the given character in all representations
|
89
|
+
# @!visibility private
|
90
|
+
def encoded(char, uri_decode, space_matches_plus)
|
91
|
+
return Regexp.escape(char) unless uri_decode
|
92
|
+
uri_parser = URI::Parser.new
|
93
|
+
encoded = uri_parser.escape(char, /./)
|
94
|
+
list = [uri_parser.escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
|
95
|
+
list << encoded('+', uri_decode, space_matches_plus) if space_matches_plus and char == " "
|
96
|
+
"(?:%s)" % list.join("|")
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Array<String>]
|
100
|
+
# @!visibility private
|
101
|
+
def capture_names
|
102
|
+
return payload.capture_names if payload.respond_to? :capture_names
|
103
|
+
return [] unless payload.respond_to? :map
|
104
|
+
payload.map { |e| e.capture_names if e.respond_to? :capture_names }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @!visibility private
|
109
|
+
class Char < Node
|
110
|
+
# @!visibility private
|
111
|
+
def compile(uri_decode: true, space_matches_plus: true, **options)
|
112
|
+
encoded(payload, uri_decode, space_matches_plus)
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!visibility private
|
116
|
+
def lookahead?(in_lookahead = false)
|
117
|
+
in_lookahead
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
121
|
+
# @!visibility private
|
122
|
+
def lookahead(ahead, options)
|
123
|
+
ahead + compile(options)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @!visibility private
|
128
|
+
class Separator < Node
|
129
|
+
# @!visibility private
|
130
|
+
def compile(options)
|
131
|
+
Regexp.escape(payload)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @!visibility private
|
135
|
+
def separator?
|
136
|
+
true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# @!visibility private
|
141
|
+
class Optional < Node
|
142
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
143
|
+
# @!visibility private
|
144
|
+
def lookahead(ahead, options)
|
145
|
+
payload.lookahead(ahead, options)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @!visibility private
|
149
|
+
def compile(options)
|
150
|
+
"(?:%s)?" % payload.compile(options)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @!visibility private
|
154
|
+
def lookahead?(in_lookahead = false)
|
155
|
+
payload.lookahead? true or payload.expect_lookahead?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @!visibility private
|
160
|
+
class Group < Node
|
161
|
+
# @!visibility private
|
162
|
+
def initialize(payload = nil, **options)
|
163
|
+
super(Array(payload), **options)
|
164
|
+
end
|
165
|
+
|
166
|
+
# @!visibility private
|
167
|
+
def lookahead?(in_lookahead = false)
|
168
|
+
return false unless payload[0..-2].all? { |e| e.lookahead? in_lookahead }
|
169
|
+
payload.last.expect_lookahead? or payload.last.lookahead? in_lookahead
|
170
|
+
end
|
171
|
+
|
172
|
+
# @!visibility private
|
173
|
+
def transform
|
174
|
+
payload.size == 1 ? payload.first.transform : super
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!visibility private
|
178
|
+
def parse
|
179
|
+
super
|
180
|
+
rescue UnexpectedClosingGroup
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
184
|
+
# @!visibility private
|
185
|
+
def lookahead(ahead, options)
|
186
|
+
payload.inject(ahead) { |a,e| e.lookahead(a, options) }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @!visibility private
|
191
|
+
class Capture < Node
|
192
|
+
# @!visibility private
|
193
|
+
def expect_lookahead?
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
# @!visibility private
|
198
|
+
def parse
|
199
|
+
self.payload ||= ""
|
200
|
+
super
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!visibility private
|
204
|
+
def capture_names
|
205
|
+
[name]
|
206
|
+
end
|
207
|
+
|
208
|
+
# @!visibility private
|
209
|
+
def name
|
210
|
+
raise CompileError, "capture name can't be empty" if payload.nil? or payload.empty?
|
211
|
+
raise CompileError, "capture name must start with underscore or lower case letter" unless payload =~ /^[a-z_]/
|
212
|
+
raise CompileError, "capture name can't be #{payload}" if payload == "splat" or payload == "captures"
|
213
|
+
payload
|
214
|
+
end
|
215
|
+
|
216
|
+
# @return [String] regexp without the named capture
|
217
|
+
# @!visibility private
|
218
|
+
def pattern(capture: nil, **options)
|
219
|
+
case capture
|
220
|
+
when Symbol then from_symbol(capture, **options)
|
221
|
+
when Array then from_array(capture, **options)
|
222
|
+
when Hash then from_hash(capture, **options)
|
223
|
+
when String then from_string(capture, **options)
|
224
|
+
when nil then from_nil(**options)
|
225
|
+
else capture
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
230
|
+
# @!visibility private
|
231
|
+
def lookahead(ahead, options)
|
232
|
+
ahead + pattern(lookahead: ahead, greedy: false, **options).to_s
|
233
|
+
end
|
234
|
+
|
235
|
+
# @!visibility private
|
236
|
+
def compile(options)
|
237
|
+
return pattern(options) if options[:no_captures]
|
238
|
+
"(?<#{name}>#{compile(no_captures: true, **options)})"
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def qualified(string, greedy: true, **options)
|
244
|
+
"#{string}+#{?? unless greedy}"
|
245
|
+
end
|
246
|
+
|
247
|
+
def default(**options)
|
248
|
+
"[^/\\?#]"
|
249
|
+
end
|
250
|
+
|
251
|
+
def from_nil(**options)
|
252
|
+
qualified(with_lookahead(default(**options), **options), **options)
|
253
|
+
end
|
254
|
+
|
255
|
+
def from_hash(hash, **options)
|
256
|
+
entry = hash[name.to_sym]
|
257
|
+
pattern(capture: entry, **options)
|
258
|
+
end
|
259
|
+
|
260
|
+
def from_array(array, **options)
|
261
|
+
array = array.map { |e| pattern(capture: e, **options) }
|
262
|
+
Regexp.union(*array)
|
263
|
+
end
|
264
|
+
|
265
|
+
def from_symbol(symbol, **options)
|
266
|
+
qualified(with_lookahead("[[:#{symbol}:]]", **options), **options)
|
267
|
+
end
|
268
|
+
|
269
|
+
def from_string(string, uri_decode: true, space_matches_plus: true, **options)
|
270
|
+
Regexp.new(string.chars.map { |c| encoded(c, uri_decode, space_matches_plus) }.join)
|
271
|
+
end
|
272
|
+
|
273
|
+
def with_lookahead(string, lookahead: nil, **options)
|
274
|
+
return string unless lookahead
|
275
|
+
"(?:(?!#{lookahead})#{string})"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# @!visibility private
|
280
|
+
class Splat < Capture
|
281
|
+
# @!visibility private
|
282
|
+
def expect_lookahead?
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
# @!visibility private
|
287
|
+
def name
|
288
|
+
"splat"
|
289
|
+
end
|
290
|
+
|
291
|
+
# @!visibility private
|
292
|
+
def pattern(options)
|
293
|
+
".*?"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# @!visibility private
|
298
|
+
class NamedSplat < Splat
|
299
|
+
alias_method :name, :payload
|
300
|
+
end
|
301
|
+
|
302
|
+
# @!visibility private
|
303
|
+
class WithLookAhead < Node
|
304
|
+
# @!visibility private
|
305
|
+
attr_accessor :head, :at_end
|
306
|
+
|
307
|
+
# @!visibility private
|
308
|
+
def initialize(payload, at_end)
|
309
|
+
self.head, *self.payload = payload
|
310
|
+
self.at_end = at_end
|
311
|
+
end
|
312
|
+
|
313
|
+
# @!visibility private
|
314
|
+
def compile(options)
|
315
|
+
lookahead = payload.inject('') { |l,e| e.lookahead(l, options) }
|
316
|
+
lookahead << (at_end ? '$' : '/')
|
317
|
+
head.compile(lookahead: lookahead, **options) + super
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# @!visibility private
|
322
|
+
class Root < Node
|
323
|
+
# @!visibility private
|
324
|
+
attr_accessor :pattern
|
325
|
+
|
326
|
+
# @!visibility private
|
327
|
+
def self.parse(string, &block)
|
328
|
+
root = new
|
329
|
+
root.pattern = string
|
330
|
+
super(root, &block).transform
|
331
|
+
end
|
332
|
+
|
333
|
+
# @!visibility private
|
334
|
+
def capture_names
|
335
|
+
super.flatten
|
336
|
+
end
|
337
|
+
|
338
|
+
# @!visibility private
|
339
|
+
def check_captures
|
340
|
+
names = capture_names
|
341
|
+
names.delete("splat")
|
342
|
+
raise CompileError, "can't use the same capture name twice" if names.uniq != names
|
343
|
+
end
|
344
|
+
|
345
|
+
# @!visibility private
|
346
|
+
def compile(except: nil, **options)
|
347
|
+
check_captures
|
348
|
+
except &&= "(?!#{except}\\Z)"
|
349
|
+
Regexp.new("\\A#{except}#{super(options)}\\Z")
|
350
|
+
rescue CompileError => e
|
351
|
+
e.message << ": #{pattern.inspect}"
|
352
|
+
raise e
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# @!visibility private
|
357
|
+
def parse(string)
|
358
|
+
buffer = StringScanner.new(string)
|
359
|
+
Root.parse(string) { parse_buffer(buffer) unless buffer.eos? }
|
360
|
+
rescue ParseError => e
|
361
|
+
e.message << " while parsing #{string.inspect}"
|
362
|
+
raise e
|
363
|
+
end
|
364
|
+
|
365
|
+
# @!visibility private
|
366
|
+
def compile(string, except: nil, **options)
|
367
|
+
options[:except] = compile(except, no_captures: true, **options) if except
|
368
|
+
parse(string).compile(options)
|
369
|
+
end
|
370
|
+
|
371
|
+
# @!visibility private
|
372
|
+
def parse_buffer(buffer)
|
373
|
+
parse_suffix(parse_element(buffer), buffer)
|
374
|
+
end
|
375
|
+
|
376
|
+
# @!visibility private
|
377
|
+
def parse_element(buffer)
|
378
|
+
raise NotImplementedError, 'subclass responsibility'
|
379
|
+
end
|
380
|
+
|
381
|
+
# @!visibility private
|
382
|
+
def parse_suffix(element, buffer)
|
383
|
+
element
|
384
|
+
end
|
385
|
+
|
386
|
+
# @!visibility private
|
387
|
+
def unexpected(char, exception: ParseError)
|
388
|
+
char = char.getch if char.respond_to? :getch
|
389
|
+
char = "space" if char == " "
|
390
|
+
raise exception, "unexpected #{char || "end of string"}"
|
391
|
+
end
|
392
|
+
|
393
|
+
# @!visibility private
|
394
|
+
def expect(buffer, regexp, **options)
|
395
|
+
regexp = Regexp.new Regexp.escape(regexp.to_str) unless regexp.is_a? Regexp
|
396
|
+
string = buffer.scan(regexp) || unexpected(buffer, **options)
|
397
|
+
regexp.names.any? ? regexp.match(string) : string
|
398
|
+
end
|
399
|
+
|
400
|
+
private :parse, :compile, :parse_buffer, :parse_element, :parse_suffix, :unexpected, :expect
|
401
|
+
private_constant :Node, :Char, :Separator, :Optional, :Group, :Capture, :Splat, :NamedSplat, :WithLookAhead, :Root
|
402
|
+
end
|
403
|
+
end
|