mustermann 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +429 -672
- data/lib/mustermann.rb +95 -20
- data/lib/mustermann/ast/boundaries.rb +44 -0
- data/lib/mustermann/ast/compiler.rb +13 -7
- data/lib/mustermann/ast/expander.rb +22 -12
- data/lib/mustermann/ast/node.rb +69 -5
- data/lib/mustermann/ast/param_scanner.rb +20 -0
- data/lib/mustermann/ast/parser.rb +138 -19
- data/lib/mustermann/ast/pattern.rb +59 -7
- data/lib/mustermann/ast/template_generator.rb +28 -0
- data/lib/mustermann/ast/transformer.rb +2 -2
- data/lib/mustermann/ast/translator.rb +20 -0
- data/lib/mustermann/ast/validation.rb +4 -3
- data/lib/mustermann/composite.rb +101 -0
- data/lib/mustermann/expander.rb +2 -2
- data/lib/mustermann/identity.rb +56 -0
- data/lib/mustermann/pattern.rb +185 -10
- data/lib/mustermann/pattern_cache.rb +49 -0
- data/lib/mustermann/regexp.rb +1 -0
- data/lib/mustermann/regexp_based.rb +18 -1
- data/lib/mustermann/regular.rb +4 -1
- data/lib/mustermann/simple_match.rb +5 -0
- data/lib/mustermann/sinatra.rb +22 -5
- data/lib/mustermann/to_pattern.rb +11 -6
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -14
- data/spec/ast_spec.rb +14 -0
- data/spec/composite_spec.rb +147 -0
- data/spec/expander_spec.rb +15 -0
- data/spec/identity_spec.rb +44 -0
- data/spec/mustermann_spec.rb +17 -2
- data/spec/pattern_spec.rb +7 -3
- data/spec/regular_spec.rb +25 -0
- data/spec/sinatra_spec.rb +184 -9
- data/spec/to_pattern_spec.rb +49 -0
- metadata +15 -180
- data/.gitignore +0 -18
- data/.rspec +0 -2
- data/.travis.yml +0 -4
- data/.yardopts +0 -1
- data/Gemfile +0 -2
- data/LICENSE +0 -22
- data/Rakefile +0 -6
- data/internals.md +0 -64
- data/lib/mustermann/ast/tree_renderer.rb +0 -29
- data/lib/mustermann/rails.rb +0 -17
- data/lib/mustermann/shell.rb +0 -29
- data/lib/mustermann/simple.rb +0 -35
- data/lib/mustermann/template.rb +0 -47
- data/spec/rails_spec.rb +0 -521
- data/spec/shell_spec.rb +0 -108
- data/spec/simple_spec.rb +0 -236
- data/spec/support.rb +0 -5
- data/spec/support/coverage.rb +0 -16
- data/spec/support/env.rb +0 -16
- data/spec/support/expand_matcher.rb +0 -27
- data/spec/support/match_matcher.rb +0 -39
- data/spec/support/pattern.rb +0 -39
- data/spec/template_spec.rb +0 -814
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module AST
|
5
|
+
# Scans an AST for param converters.
|
6
|
+
# @!visibility private
|
7
|
+
# @see Mustermann::AST::Pattern#to_templates
|
8
|
+
class ParamScanner < Translator
|
9
|
+
# @!visibility private
|
10
|
+
def self.scan_params(ast)
|
11
|
+
new.translate(ast)
|
12
|
+
end
|
13
|
+
|
14
|
+
translate(:node) { t(payload) }
|
15
|
+
translate(Array) { map { |e| t(e) }.inject(:merge) }
|
16
|
+
translate(Object) { {} }
|
17
|
+
translate(:capture) { convert ? { name => convert } : {} }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -11,8 +11,8 @@ module Mustermann
|
|
11
11
|
# @param [String] string to be parsed
|
12
12
|
# @return [Mustermann::AST::Node] parse tree for string
|
13
13
|
# @!visibility private
|
14
|
-
def self.parse(string)
|
15
|
-
new.parse(string)
|
14
|
+
def self.parse(string, **options)
|
15
|
+
new(**options).parse(string)
|
16
16
|
end
|
17
17
|
|
18
18
|
# Defines another grammar rule for first character.
|
@@ -31,24 +31,29 @@ module Mustermann
|
|
31
31
|
#
|
32
32
|
# @see Mustermann::Sinatra
|
33
33
|
# @!visibility private
|
34
|
-
def self.suffix(pattern = /./, &block)
|
34
|
+
def self.suffix(pattern = /./, after: :node, &block)
|
35
35
|
@suffix ||= []
|
36
|
-
@suffix << [pattern, block] if block
|
36
|
+
@suffix << [pattern, after, block] if block
|
37
37
|
@suffix
|
38
38
|
end
|
39
39
|
|
40
40
|
# @!visibility private
|
41
|
-
attr_reader :buffer, :string
|
41
|
+
attr_reader :buffer, :string, :pattern
|
42
42
|
|
43
43
|
extend Forwardable
|
44
|
-
def_delegators :buffer, :eos?, :getch
|
44
|
+
def_delegators :buffer, :eos?, :getch, :pos
|
45
|
+
|
46
|
+
# @!visibility private
|
47
|
+
def initialize(pattern: nil, **options)
|
48
|
+
@pattern = pattern
|
49
|
+
end
|
45
50
|
|
46
51
|
# @param [String] string to be parsed
|
47
52
|
# @return [Mustermann::AST::Node] parse tree for string
|
48
53
|
# @!visibility private
|
49
54
|
def parse(string)
|
50
55
|
@string = string
|
51
|
-
@buffer = StringScanner.new(string)
|
56
|
+
@buffer = ::StringScanner.new(string)
|
52
57
|
node(:root, string) { read unless eos? }
|
53
58
|
end
|
54
59
|
|
@@ -59,8 +64,10 @@ module Mustermann
|
|
59
64
|
# @return [Mustermann::AST::Node]
|
60
65
|
# @!visibility private
|
61
66
|
def node(type, *args, &block)
|
62
|
-
type
|
63
|
-
|
67
|
+
type = Node[type] unless type.respond_to? :new
|
68
|
+
start = pos
|
69
|
+
node = block ? type.parse(*args, &block) : type.new(*args)
|
70
|
+
min_size(start, pos, node)
|
64
71
|
end
|
65
72
|
|
66
73
|
# Create a node for a character we don't have an explicit rule for.
|
@@ -76,20 +83,35 @@ module Mustermann
|
|
76
83
|
# @return [Mustermann::AST::Node] next element
|
77
84
|
# @!visibility private
|
78
85
|
def read
|
79
|
-
|
80
|
-
|
81
|
-
|
86
|
+
start = pos
|
87
|
+
char = getch
|
88
|
+
method = "read %p" % char
|
89
|
+
element= respond_to?(method) ? send(method, char) : default_node(char)
|
90
|
+
min_size(start, pos, element)
|
82
91
|
read_suffix(element)
|
83
92
|
end
|
84
93
|
|
94
|
+
# sets start on node to start if it's not set to a lower value.
|
95
|
+
# sets stop on node to stop if it's not set to a higher value.
|
96
|
+
# @return [Mustermann::AST::Node] the node passed as third argument
|
97
|
+
# @!visibility private
|
98
|
+
def min_size(start, stop, node)
|
99
|
+
stop ||= start
|
100
|
+
start ||= stop
|
101
|
+
node.start = start unless node.start and node.start < start
|
102
|
+
node.stop = stop unless node.stop and node.stop > stop
|
103
|
+
node
|
104
|
+
end
|
105
|
+
|
85
106
|
# Checks for a potential suffix on the buffer.
|
86
107
|
# @param [Mustermann::AST::Node] element node without suffix
|
87
108
|
# @return [Mustermann::AST::Node] node with suffix
|
88
109
|
# @!visibility private
|
89
110
|
def read_suffix(element)
|
90
|
-
self.class.suffix.inject(element) do |ele, (regexp, callback)|
|
91
|
-
next ele unless payload = scan(regexp)
|
92
|
-
instance_exec(payload, ele, &callback)
|
111
|
+
self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
|
112
|
+
next ele unless ele.is_a?(after) and payload = scan(regexp)
|
113
|
+
content = instance_exec(payload, ele, &callback)
|
114
|
+
min_size(element.start, pos, content)
|
93
115
|
end
|
94
116
|
end
|
95
117
|
|
@@ -102,8 +124,25 @@ module Mustermann
|
|
102
124
|
# @return [String, MatchData, nil]
|
103
125
|
# @!visibility private
|
104
126
|
def scan(regexp)
|
127
|
+
match_buffer(:scan, regexp)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Wrapper around {StringScanner#check} that turns strings into escaped
|
131
|
+
# regular expressions and returns a MatchData if the regexp has any
|
132
|
+
# named captures.
|
133
|
+
#
|
134
|
+
# @param [Regexp, String] regexp
|
135
|
+
# @see StringScanner#check
|
136
|
+
# @return [String, MatchData, nil]
|
137
|
+
# @!visibility private
|
138
|
+
def check(regexp)
|
139
|
+
match_buffer(:check, regexp)
|
140
|
+
end
|
141
|
+
|
142
|
+
# @!visibility private
|
143
|
+
def match_buffer(method, regexp)
|
105
144
|
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
|
106
|
-
string = buffer.
|
145
|
+
string = buffer.public_send(method, regexp)
|
107
146
|
regexp.names.any? ? regexp.match(string) : string
|
108
147
|
end
|
109
148
|
|
@@ -114,8 +153,85 @@ module Mustermann
|
|
114
153
|
# @return [String, MatchData] the match
|
115
154
|
# @raise [Mustermann::ParseError] if expectation wasn't met
|
116
155
|
# @!visibility private
|
117
|
-
def expect(regexp, **options)
|
118
|
-
scan(regexp)|| unexpected(**options)
|
156
|
+
def expect(regexp, char: nil, **options)
|
157
|
+
scan(regexp) || unexpected(char, **options)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Allows to read a string inside brackets. It does not expect the string
|
161
|
+
# to start with an opening bracket.
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# buffer.string = "fo<o>>ba<r>"
|
165
|
+
# read_brackets(?<, ?>) # => "fo<o>"
|
166
|
+
# buffer.rest # => "ba<r>"
|
167
|
+
#
|
168
|
+
# @!visibility private
|
169
|
+
def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options)
|
170
|
+
result = ""
|
171
|
+
escape = false if escape.nil?
|
172
|
+
while current = getch
|
173
|
+
case current
|
174
|
+
when close then return result
|
175
|
+
when open then result << open << read_brackets(open, close) << close
|
176
|
+
when escape then result << escape << getch
|
177
|
+
else result << current
|
178
|
+
end
|
179
|
+
end
|
180
|
+
unexpected(char, **options)
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Reads an argument string of the format arg1,args2,key:value
|
185
|
+
#
|
186
|
+
# @!visibility private
|
187
|
+
def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options)
|
188
|
+
list, map = [], {}
|
189
|
+
while buffer.peek(1) != close
|
190
|
+
scan(separator)
|
191
|
+
entries = read_list(close, separator, separator: key_separator, **options)
|
192
|
+
case entries.size
|
193
|
+
when 1 then list += entries
|
194
|
+
when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
|
195
|
+
else unexpected(key_separator)
|
196
|
+
end
|
197
|
+
buffer.pos -= 1
|
198
|
+
end
|
199
|
+
expect(close)
|
200
|
+
[list, map]
|
201
|
+
end
|
202
|
+
|
203
|
+
# Reads a separated list with the ability to quote, escape and add spaces.
|
204
|
+
#
|
205
|
+
# @!visibility private
|
206
|
+
def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options)
|
207
|
+
result = []
|
208
|
+
while current = getch
|
209
|
+
element = result.empty? ? result : result.last
|
210
|
+
case current
|
211
|
+
when *close then return result
|
212
|
+
when ignore then nil # do nothing
|
213
|
+
when separator then result << ""
|
214
|
+
when escape then element << getch
|
215
|
+
when *quotes then element << read_escaped(current, escape: escape)
|
216
|
+
else element << current
|
217
|
+
end
|
218
|
+
end
|
219
|
+
unexpected(current, **options)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Read a string until a terminating character, ignoring escaped versions of said character.
|
223
|
+
#
|
224
|
+
# @!visibility private
|
225
|
+
def read_escaped(close, escape: ?\\, **options)
|
226
|
+
result = ""
|
227
|
+
while current = getch
|
228
|
+
case current
|
229
|
+
when close then return result
|
230
|
+
when escape then result << getch
|
231
|
+
else result << current
|
232
|
+
end
|
233
|
+
end
|
234
|
+
unexpected(current, **options)
|
119
235
|
end
|
120
236
|
|
121
237
|
# Helper for raising an exception for an unexpected character.
|
@@ -124,10 +240,13 @@ module Mustermann
|
|
124
240
|
# @param [String, nil] char the unexpected character
|
125
241
|
# @raise [Mustermann::ParseError, Exception]
|
126
242
|
# @!visibility private
|
127
|
-
def unexpected(char =
|
243
|
+
def unexpected(char = nil, exception: ParseError)
|
244
|
+
char ||= getch
|
128
245
|
char = "space" if char == " "
|
129
246
|
raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
|
130
247
|
end
|
248
|
+
|
249
|
+
private :match_buffer
|
131
250
|
end
|
132
251
|
|
133
252
|
private_constant :Parser
|
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'mustermann/ast/parser'
|
2
|
+
require 'mustermann/ast/boundaries'
|
2
3
|
require 'mustermann/ast/compiler'
|
3
4
|
require 'mustermann/ast/transformer'
|
4
5
|
require 'mustermann/ast/validation'
|
6
|
+
require 'mustermann/ast/template_generator'
|
7
|
+
require 'mustermann/ast/param_scanner'
|
5
8
|
require 'mustermann/regexp_based'
|
6
9
|
require 'mustermann/expander'
|
7
10
|
require 'tool/equality_map'
|
@@ -16,8 +19,9 @@ module Mustermann
|
|
16
19
|
|
17
20
|
extend Forwardable, SingleForwardable
|
18
21
|
single_delegate on: :parser, suffix: :parser
|
19
|
-
instance_delegate %i[parser compiler transformer validation] => 'self.class'
|
20
|
-
instance_delegate parse: :parser, transform: :transformer, validate: :validation
|
22
|
+
instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class'
|
23
|
+
instance_delegate parse: :parser, transform: :transformer, validate: :validation,
|
24
|
+
generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries
|
21
25
|
|
22
26
|
# @api private
|
23
27
|
# @return [#parse] parser object for pattern
|
@@ -36,7 +40,14 @@ module Mustermann
|
|
36
40
|
end
|
37
41
|
|
38
42
|
# @api private
|
39
|
-
# @return [#
|
43
|
+
# @return [#set_boundaries] translator making sure start and stop is set on all nodes
|
44
|
+
# @!visibility private
|
45
|
+
def self.boundaries
|
46
|
+
Boundaries
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
# @return [#transform] transformer object for pattern
|
40
51
|
# @!visibility private
|
41
52
|
def self.transformer
|
42
53
|
Transformer
|
@@ -49,6 +60,20 @@ module Mustermann
|
|
49
60
|
Validation
|
50
61
|
end
|
51
62
|
|
63
|
+
# @api private
|
64
|
+
# @return [#generate_templates] generates URI templates for pattern
|
65
|
+
# @!visibility private
|
66
|
+
def self.template_generator
|
67
|
+
TemplateGenerator
|
68
|
+
end
|
69
|
+
|
70
|
+
# @api private
|
71
|
+
# @return [#scan_params] param scanner for pattern
|
72
|
+
# @!visibility private
|
73
|
+
def self.param_scanner
|
74
|
+
ParamScanner
|
75
|
+
end
|
76
|
+
|
52
77
|
# @!visibility private
|
53
78
|
def compile(**options)
|
54
79
|
options[:except] &&= parse options[:except]
|
@@ -62,7 +87,12 @@ module Mustermann
|
|
62
87
|
# @!visibility private
|
63
88
|
def to_ast
|
64
89
|
@ast_cache ||= Tool::EqualityMap.new
|
65
|
-
@ast_cache.fetch(@string)
|
90
|
+
@ast_cache.fetch(@string) do
|
91
|
+
ast = parse(@string, pattern: self)
|
92
|
+
ast &&= transform(ast)
|
93
|
+
ast &&= set_boundaries(ast, string: @string)
|
94
|
+
validate(ast)
|
95
|
+
end
|
66
96
|
end
|
67
97
|
|
68
98
|
# All AST-based pattern implementations support expanding.
|
@@ -73,12 +103,34 @@ module Mustermann
|
|
73
103
|
# @raise (see Mustermann::Pattern#expand)
|
74
104
|
# @see Mustermann::Pattern#expand
|
75
105
|
# @see Mustermann::Expander
|
76
|
-
def expand(
|
106
|
+
def expand(behavior = nil, values = {})
|
77
107
|
@expander ||= Mustermann::Expander.new(self)
|
78
|
-
@expander.expand(
|
108
|
+
@expander.expand(behavior, values)
|
109
|
+
end
|
110
|
+
|
111
|
+
# All AST-based pattern implementations support generating templates.
|
112
|
+
#
|
113
|
+
# @example (see Mustermann::Pattern#to_templates)
|
114
|
+
# @param (see Mustermann::Pattern#to_templates)
|
115
|
+
# @return (see Mustermann::Pattern#to_templates)
|
116
|
+
# @see Mustermann::Pattern#to_templates
|
117
|
+
def to_templates
|
118
|
+
@to_templates ||= generate_templates(to_ast)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @!visibility private
|
122
|
+
# @see Mustermann::Pattern#map_param
|
123
|
+
def map_param(key, value)
|
124
|
+
return super unless param_converters.include? key
|
125
|
+
param_converters[key][super]
|
126
|
+
end
|
127
|
+
|
128
|
+
# @!visibility private
|
129
|
+
def param_converters
|
130
|
+
@param_converters ||= scan_params(to_ast)
|
79
131
|
end
|
80
132
|
|
81
|
-
private :compile
|
133
|
+
private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
|
82
134
|
end
|
83
135
|
end
|
84
136
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module AST
|
5
|
+
# Turns an AST into an Array of URI templates representing the AST.
|
6
|
+
# @!visibility private
|
7
|
+
# @see Mustermann::AST::Pattern#to_templates
|
8
|
+
class TemplateGenerator < Translator
|
9
|
+
# @!visibility private
|
10
|
+
def self.generate_templates(ast)
|
11
|
+
new.translate(ast).uniq
|
12
|
+
end
|
13
|
+
|
14
|
+
# translate(:expression) is not needed, since template patterns simply call to_s
|
15
|
+
translate(:root, :group) { t(payload) || [""] }
|
16
|
+
translate(:separator, :char) { t.escape(payload) }
|
17
|
+
translate(:capture) { "{#{name}}" }
|
18
|
+
translate(:optional) { [t(payload), ""] }
|
19
|
+
translate(:named_splat, :splat) { "{+#{name}}" }
|
20
|
+
translate(:with_look_ahead) { t([head, payload]) }
|
21
|
+
translate(:union) { payload.flat_map { |e| t(e) } }
|
22
|
+
|
23
|
+
translate(Array) do
|
24
|
+
map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -44,7 +44,7 @@ module Mustermann
|
|
44
44
|
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
|
45
45
|
separator = Node[:separator].new(operator.separator)
|
46
46
|
prefix = Node[:separator].new(operator.prefix)
|
47
|
-
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
|
47
|
+
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) })
|
48
48
|
payload.unshift(prefix) if operator.prefix
|
49
49
|
self
|
50
50
|
end
|
@@ -91,7 +91,7 @@ module Mustermann
|
|
91
91
|
# @!visibility private
|
92
92
|
def create_lookahead(elements, *args)
|
93
93
|
return elements unless elements.size > 1
|
94
|
-
[Node[:with_look_ahead].new(elements, *args)]
|
94
|
+
[Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)]
|
95
95
|
end
|
96
96
|
|
97
97
|
# can the given element be used in a look-ahead?
|
@@ -74,6 +74,26 @@ module Mustermann
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
+
# Enables quick creation of a translator object.
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# require 'mustermann'
|
81
|
+
# require 'mustermann/ast/translator'
|
82
|
+
#
|
83
|
+
# translator = Mustermann::AST::Translator.create do
|
84
|
+
# translate(:node) { [type, *t(payload)].flatten.compact }
|
85
|
+
# translate(Array) { map { |e| t(e) } }
|
86
|
+
# translate(Object) { }
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# ast = Mustermann.new('/:name').to_ast
|
90
|
+
# translator.translate(ast) # => [:root, :separator, :capture]
|
91
|
+
#
|
92
|
+
# @!visibility private
|
93
|
+
def self.create(&block)
|
94
|
+
Class.new(self, &block).new
|
95
|
+
end
|
96
|
+
|
77
97
|
raises Mustermann::Error
|
78
98
|
|
79
99
|
# @param [Mustermann::AST::Node, Object] node to translate
|
@@ -21,14 +21,15 @@ module Mustermann
|
|
21
21
|
translate(Object, :splat) {}
|
22
22
|
translate(:node) { t(payload) }
|
23
23
|
translate(Array) { each { |p| t(p)} }
|
24
|
-
translate(:capture
|
24
|
+
translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])}
|
25
|
+
translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')}
|
25
26
|
|
26
27
|
# @raise [Mustermann::CompileError] if name is not acceptable
|
27
28
|
# @!visibility private
|
28
|
-
def check_name(name)
|
29
|
+
def check_name(name, forbidden: [])
|
29
30
|
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
|
30
31
|
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
|
31
|
-
raise CompileError, "capture name can't be #{name}" if
|
32
|
+
raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
|
32
33
|
raise CompileError, "can't use the same capture name twice" if names.include? name
|
33
34
|
names << name
|
34
35
|
end
|