regexp_parser 2.12.0 → 2.13.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/LICENSE +1 -1
- data/lib/regexp_parser/expression/base.rb +50 -14
- data/lib/regexp_parser/expression/classes/character_set/range.rb +1 -1
- data/lib/regexp_parser/expression/classes/character_set.rb +1 -1
- data/lib/regexp_parser/expression/classes/unicode_property.rb +1 -1
- data/lib/regexp_parser/expression/methods/match_length.rb +15 -7
- data/lib/regexp_parser/expression/methods/tests.rb +20 -4
- data/lib/regexp_parser/expression/methods/traverse.rb +51 -13
- data/lib/regexp_parser/expression/quantifier.rb +1 -1
- data/lib/regexp_parser/expression/shared.rb +24 -22
- data/lib/regexp_parser/expression/subexpression.rb +2 -2
- data/lib/regexp_parser/lexer.rb +21 -22
- data/lib/regexp_parser/parser.rb +12 -36
- data/lib/regexp_parser/scanner.rb +184 -236
- data/lib/regexp_parser/syntax/version_lookup.rb +25 -10
- data/lib/regexp_parser/version.rb +1 -1
- data/regexp_parser.gemspec +3 -9
- metadata +2 -9
- data/Gemfile +0 -17
- data/Rakefile +0 -25
- data/lib/regexp_parser/scanner/char_type.rl +0 -28
- data/lib/regexp_parser/scanner/property.rl +0 -30
- data/lib/regexp_parser/scanner/scanner.rl +0 -864
|
@@ -1,864 +0,0 @@
|
|
|
1
|
-
%%{
|
|
2
|
-
machine re_scanner;
|
|
3
|
-
include re_char_type "char_type.rl";
|
|
4
|
-
include re_property "property.rl";
|
|
5
|
-
|
|
6
|
-
utf8_2_byte = (0xc2..0xdf 0x80..0xbf);
|
|
7
|
-
utf8_3_byte = (0xe0..0xef 0x80..0xbf 0x80..0xbf);
|
|
8
|
-
utf8_4_byte = (0xf0..0xf4 0x80..0xbf 0x80..0xbf 0x80..0xbf);
|
|
9
|
-
utf8_multibyte = utf8_2_byte | utf8_3_byte | utf8_4_byte;
|
|
10
|
-
|
|
11
|
-
dot = '.';
|
|
12
|
-
backslash = '\\';
|
|
13
|
-
alternation = '|';
|
|
14
|
-
beginning_of_line = '^';
|
|
15
|
-
end_of_line = '$';
|
|
16
|
-
|
|
17
|
-
range_open = '{';
|
|
18
|
-
range_close = '}';
|
|
19
|
-
curlies = range_open | range_close;
|
|
20
|
-
|
|
21
|
-
group_open = '(';
|
|
22
|
-
group_close = ')';
|
|
23
|
-
parentheses = group_open | group_close;
|
|
24
|
-
|
|
25
|
-
set_open = '[';
|
|
26
|
-
set_close = ']';
|
|
27
|
-
brackets = set_open | set_close;
|
|
28
|
-
|
|
29
|
-
comment = ('#' . [^\n]* . '\n'?);
|
|
30
|
-
|
|
31
|
-
class_posix = ('[:' . '^'? . [^\[\]]* . ':]');
|
|
32
|
-
|
|
33
|
-
line_anchor = beginning_of_line | end_of_line;
|
|
34
|
-
anchor_char = [AbBzZG];
|
|
35
|
-
|
|
36
|
-
escaped_ascii = [abefnrtv];
|
|
37
|
-
octal_sequence = [0-7]{1,3};
|
|
38
|
-
|
|
39
|
-
hex_sequence = 'x' . xdigit{1,2};
|
|
40
|
-
hex_sequence_err = 'x' . [^0-9A-Fa-f];
|
|
41
|
-
high_hex_sequence = 'x' . [89A-Fa-f] . xdigit . ( '\\x' . [89A-Fa-f] . xdigit )*;
|
|
42
|
-
|
|
43
|
-
codepoint_single = 'u' . xdigit{4};
|
|
44
|
-
codepoint_list = 'u{' . xdigit{1,6} . (space . xdigit{1,6})* . '}';
|
|
45
|
-
codepoint_sequence = codepoint_single | codepoint_list;
|
|
46
|
-
|
|
47
|
-
control_sequence = ('c' | 'C-') . (backslash . 'M-')? . backslash? . any;
|
|
48
|
-
|
|
49
|
-
meta_sequence = 'M-' . (backslash . ('c' | 'C-'))? . backslash? . any;
|
|
50
|
-
|
|
51
|
-
sequence_char = [CMcux];
|
|
52
|
-
|
|
53
|
-
zero_or_one = '?' | '??' | '?+';
|
|
54
|
-
zero_or_more = '*' | '*?' | '*+';
|
|
55
|
-
one_or_more = '+' | '+?' | '++';
|
|
56
|
-
|
|
57
|
-
quantifier_greedy = '?' | '*' | '+';
|
|
58
|
-
|
|
59
|
-
quantity_exact = (digit+);
|
|
60
|
-
quantity_minimum = (digit+) . ',';
|
|
61
|
-
quantity_maximum = ',' . (digit+);
|
|
62
|
-
quantity_range = (digit+) . ',' . (digit+);
|
|
63
|
-
quantifier_interval = range_open . ( quantity_exact | quantity_minimum |
|
|
64
|
-
quantity_maximum | quantity_range ) . range_close;
|
|
65
|
-
|
|
66
|
-
conditional = '(?(';
|
|
67
|
-
|
|
68
|
-
group_comment = '?#' . [^)]* . group_close;
|
|
69
|
-
|
|
70
|
-
group_atomic = '?>';
|
|
71
|
-
group_passive = '?:';
|
|
72
|
-
group_absence = '?~';
|
|
73
|
-
|
|
74
|
-
assertion_lookahead = '?=';
|
|
75
|
-
assertion_nlookahead = '?!';
|
|
76
|
-
assertion_lookbehind = '?<=';
|
|
77
|
-
assertion_nlookbehind = '?<!';
|
|
78
|
-
|
|
79
|
-
# try to treat every other group head as options group, like Ruby
|
|
80
|
-
group_options = '?' . ( [^!#'():<=>~]+ . ':'? ) ?;
|
|
81
|
-
|
|
82
|
-
group_name_id_ab = ([^!=0-9\->] | utf8_multibyte) . ([^>] | utf8_multibyte)*;
|
|
83
|
-
group_name_id_sq = ([^0-9\-'] | utf8_multibyte) . ([^'] | utf8_multibyte)*;
|
|
84
|
-
group_number = '-'? . [0-9]+;
|
|
85
|
-
group_level = [+\-] . [0-9]+;
|
|
86
|
-
|
|
87
|
-
group_name = ('<' . group_name_id_ab? . '>') |
|
|
88
|
-
("'" . group_name_id_sq? . "'");
|
|
89
|
-
group_lookup = group_name | group_number;
|
|
90
|
-
|
|
91
|
-
group_named = ('?' . group_name );
|
|
92
|
-
|
|
93
|
-
group_ref_body = (('<' . (group_name_id_ab? | group_number) . group_level? '>') |
|
|
94
|
-
("'" . (group_name_id_sq? | group_number) . group_level? "'"));
|
|
95
|
-
|
|
96
|
-
group_ref = 'k' . group_ref_body;
|
|
97
|
-
group_call = 'g' . group_ref_body;
|
|
98
|
-
|
|
99
|
-
group_type = group_atomic | group_passive | group_absence | group_named;
|
|
100
|
-
|
|
101
|
-
keep_mark = 'K';
|
|
102
|
-
|
|
103
|
-
assertion_type = assertion_lookahead | assertion_nlookahead |
|
|
104
|
-
assertion_lookbehind | assertion_nlookbehind;
|
|
105
|
-
|
|
106
|
-
# characters that 'break' a literal
|
|
107
|
-
meta_char = dot | backslash | alternation |
|
|
108
|
-
curlies | parentheses | brackets |
|
|
109
|
-
line_anchor | quantifier_greedy;
|
|
110
|
-
|
|
111
|
-
literal_delimiters = ']' | '}';
|
|
112
|
-
|
|
113
|
-
ascii_print = ((0x20..0x7e) - meta_char - '#');
|
|
114
|
-
ascii_nonprint = (0x01..0x1f | 0x7f);
|
|
115
|
-
|
|
116
|
-
non_literal_escape = char_type_char | anchor_char | escaped_ascii |
|
|
117
|
-
keep_mark | sequence_char;
|
|
118
|
-
|
|
119
|
-
# escapes that also work within a character set
|
|
120
|
-
set_escape = backslash | brackets | escaped_ascii |
|
|
121
|
-
octal_sequence | property_char |
|
|
122
|
-
sequence_char | single_codepoint_char_type;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# EOF error, used where it can be detected
|
|
126
|
-
action premature_end_error {
|
|
127
|
-
text = copy(data, ts ? ts-1 : 0, -1)
|
|
128
|
-
raise PrematureEndError.new(text)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
# Invalid sequence error, used from sequences, like escapes and sets
|
|
132
|
-
action invalid_sequence_error {
|
|
133
|
-
text = copy(data, ts ? ts-1 : 0, -1)
|
|
134
|
-
raise ValidationError.for(:sequence, 'sequence', text)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
# group (nesting) and set open/close actions
|
|
138
|
-
action group_opened { self.group_depth = group_depth + 1 }
|
|
139
|
-
action group_closed { self.group_depth = group_depth - 1 }
|
|
140
|
-
action set_opened { self.set_depth = set_depth + 1 }
|
|
141
|
-
action set_closed { self.set_depth = set_depth - 1 }
|
|
142
|
-
|
|
143
|
-
# Character set scanner, continues consuming characters until it meets the
|
|
144
|
-
# closing bracket of the set.
|
|
145
|
-
# --------------------------------------------------------------------------
|
|
146
|
-
character_set := |*
|
|
147
|
-
set_close > (set_meta, 2) @set_closed {
|
|
148
|
-
emit(:set, :close, copy(data, ts, te))
|
|
149
|
-
if in_set?
|
|
150
|
-
fret;
|
|
151
|
-
else
|
|
152
|
-
fgoto main;
|
|
153
|
-
end
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
'-]' @set_closed { # special case, emits two tokens
|
|
157
|
-
emit(:literal, :literal, '-')
|
|
158
|
-
emit(:set, :close, ']')
|
|
159
|
-
if in_set?
|
|
160
|
-
fret;
|
|
161
|
-
else
|
|
162
|
-
fgoto main;
|
|
163
|
-
end
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
'-&&' { # special case, emits two tokens
|
|
167
|
-
emit(:literal, :literal, '-')
|
|
168
|
-
emit(:set, :intersection, '&&')
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
'^' {
|
|
172
|
-
if prev_token[1] == :open
|
|
173
|
-
emit(:set, :negate, '^')
|
|
174
|
-
else
|
|
175
|
-
emit(:literal, :literal, '^')
|
|
176
|
-
end
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
'-' {
|
|
180
|
-
# ranges cant start with the opening bracket, a subset, or
|
|
181
|
-
# intersection/negation/range operators
|
|
182
|
-
if prev_token[0] == :set
|
|
183
|
-
emit(:literal, :literal, '-')
|
|
184
|
-
else
|
|
185
|
-
emit(:set, :range, '-')
|
|
186
|
-
end
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
# Unlike ranges, intersections can start or end at set boundaries, whereupon
|
|
190
|
-
# they match nothing: r = /[a&&]/; [r =~ ?a, r =~ ?&] # => [nil, nil]
|
|
191
|
-
'&&' {
|
|
192
|
-
emit(:set, :intersection, '&&')
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
backslash {
|
|
196
|
-
fcall set_escape_sequence;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
set_open >(open_bracket, 1) >set_opened {
|
|
200
|
-
emit(:set, :open, '[')
|
|
201
|
-
fcall character_set;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
class_posix >(open_bracket, 1) @set_closed @eof(premature_end_error) {
|
|
205
|
-
text = copy(data, ts, te)
|
|
206
|
-
|
|
207
|
-
type = :posixclass
|
|
208
|
-
class_name = text[2..-3]
|
|
209
|
-
if class_name[0] == '^'
|
|
210
|
-
class_name = class_name[1..-1]
|
|
211
|
-
type = :nonposixclass
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
unless POSIX_CLASSES[class_name]
|
|
215
|
-
raise ValidationError.for(:posix_class, text)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
emit(type, class_name.to_sym, text)
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
meta_char > (set_meta, 1) {
|
|
222
|
-
emit(:literal, :literal, copy(data, ts, te))
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
any | ascii_nonprint | utf8_multibyte {
|
|
226
|
-
text = copy(data, ts, te)
|
|
227
|
-
emit(:literal, :literal, text)
|
|
228
|
-
};
|
|
229
|
-
*|;
|
|
230
|
-
|
|
231
|
-
# set escapes scanner
|
|
232
|
-
# --------------------------------------------------------------------------
|
|
233
|
-
set_escape_sequence := |*
|
|
234
|
-
# Special case: in sets, octal sequences have higher priority than backrefs
|
|
235
|
-
octal_sequence {
|
|
236
|
-
emit(:escape, :octal, copy(data, ts-1, te))
|
|
237
|
-
fret;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
# Scan all other escapes that work in sets with the generic escape scanner
|
|
241
|
-
set_escape > (escaped_set_alpha, 2) {
|
|
242
|
-
fhold;
|
|
243
|
-
fnext character_set;
|
|
244
|
-
fcall escape_sequence;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
# Treat all remaining escapes - those not supported in sets - as literal.
|
|
248
|
-
# (This currently includes \^, \-, \&, \:, although these could potentially
|
|
249
|
-
# be meta chars when not escaped, depending on their position in the set.)
|
|
250
|
-
(any | utf8_multibyte) > (escaped_set_alpha, 1) {
|
|
251
|
-
emit(:escape, :literal, copy(data, ts-1, te))
|
|
252
|
-
fret;
|
|
253
|
-
};
|
|
254
|
-
*|;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# escape sequence scanner
|
|
258
|
-
# --------------------------------------------------------------------------
|
|
259
|
-
escape_sequence := |*
|
|
260
|
-
[1-9] . [0-9]* {
|
|
261
|
-
text = copy(data, ts-1, te)
|
|
262
|
-
|
|
263
|
-
# If not enough groups have been opened, there is a fallback to either an
|
|
264
|
-
# octal or literal interpretation for 2+ digit numerical escapes.
|
|
265
|
-
digits = text[1..-1]
|
|
266
|
-
if digits.size == 1 || digits.to_i <= capturing_group_count
|
|
267
|
-
emit(:backref, :number, text)
|
|
268
|
-
elsif digits =~ /\A[0-7]{2,}\z/
|
|
269
|
-
emit(:escape, :octal, text)
|
|
270
|
-
else
|
|
271
|
-
emit(:escape, :literal, text[0..1])
|
|
272
|
-
emit(:literal, :literal, text[2..-1])
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
fret;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
octal_sequence {
|
|
279
|
-
emit(:escape, :octal, copy(data, ts-1, te))
|
|
280
|
-
fret;
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
[8-9] . [0-9] { # special case, emits two tokens
|
|
284
|
-
text = copy(data, ts-1, te)
|
|
285
|
-
emit(:escape, :literal, text[0, 2])
|
|
286
|
-
emit(:literal, :literal, text[2])
|
|
287
|
-
fret;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
meta_char {
|
|
291
|
-
case text = copy(data, ts-1, te)
|
|
292
|
-
when '\.'; emit(:escape, :dot, text)
|
|
293
|
-
when '\|'; emit(:escape, :alternation, text)
|
|
294
|
-
when '\^'; emit(:escape, :bol, text)
|
|
295
|
-
when '\$'; emit(:escape, :eol, text)
|
|
296
|
-
when '\?'; emit(:escape, :zero_or_one, text)
|
|
297
|
-
when '\*'; emit(:escape, :zero_or_more, text)
|
|
298
|
-
when '\+'; emit(:escape, :one_or_more, text)
|
|
299
|
-
when '\('; emit(:escape, :group_open, text)
|
|
300
|
-
when '\)'; emit(:escape, :group_close, text)
|
|
301
|
-
when '\{'; emit(:escape, :interval_open, text)
|
|
302
|
-
when '\}'; emit(:escape, :interval_close, text)
|
|
303
|
-
when '\['; emit(:escape, :set_open, text)
|
|
304
|
-
when '\]'; emit(:escape, :set_close, text)
|
|
305
|
-
when "\\\\";
|
|
306
|
-
emit(:escape, :backslash, text)
|
|
307
|
-
end
|
|
308
|
-
fret;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
escaped_ascii > (escaped_alpha, 7) {
|
|
312
|
-
# \b is emitted as backspace only when inside a character set, otherwise
|
|
313
|
-
# it is a word boundary anchor. A syntax might "normalize" it if needed.
|
|
314
|
-
case text = copy(data, ts-1, te)
|
|
315
|
-
when '\a'; emit(:escape, :bell, text)
|
|
316
|
-
when '\b'; emit(:escape, :backspace, text)
|
|
317
|
-
when '\e'; emit(:escape, :escape, text)
|
|
318
|
-
when '\f'; emit(:escape, :form_feed, text)
|
|
319
|
-
when '\n'; emit(:escape, :newline, text)
|
|
320
|
-
when '\r'; emit(:escape, :carriage, text)
|
|
321
|
-
when '\t'; emit(:escape, :tab, text)
|
|
322
|
-
when '\v'; emit(:escape, :vertical_tab, text)
|
|
323
|
-
end
|
|
324
|
-
fret;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
codepoint_sequence > (escaped_alpha, 6) $eof(premature_end_error) {
|
|
328
|
-
text = copy(data, ts-1, te)
|
|
329
|
-
if text[2] == '{'
|
|
330
|
-
emit(:escape, :codepoint_list, text)
|
|
331
|
-
else
|
|
332
|
-
emit(:escape, :codepoint, text)
|
|
333
|
-
end
|
|
334
|
-
fret;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
high_hex_sequence > (escaped_alpha, 5) {
|
|
338
|
-
text = copy(data, ts-1, te)
|
|
339
|
-
if regexp_encoding == Encoding::BINARY
|
|
340
|
-
text.split(/(?=\\)/).each { |part| emit(:escape, :hex, part) }
|
|
341
|
-
else
|
|
342
|
-
emit(:escape, :utf8_hex, text)
|
|
343
|
-
end
|
|
344
|
-
fret;
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
hex_sequence > (escaped_alpha, 5) @eof(premature_end_error) {
|
|
348
|
-
emit(:escape, :hex, copy(data, ts-1, te))
|
|
349
|
-
fret;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
hex_sequence_err @invalid_sequence_error {
|
|
353
|
-
fret;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
control_sequence >(escaped_alpha, 4) $eof(premature_end_error) {
|
|
357
|
-
emit_meta_control_sequence(data, ts, te, :control)
|
|
358
|
-
fret;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
meta_sequence >(backslashed, 3) $eof(premature_end_error) {
|
|
362
|
-
emit_meta_control_sequence(data, ts, te, :meta_sequence)
|
|
363
|
-
fret;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
char_type_char > (escaped_alpha, 2) {
|
|
367
|
-
fhold;
|
|
368
|
-
fnext *(in_set? ? fentry(character_set) : fentry(main));
|
|
369
|
-
fcall char_type;
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
property_char > (escaped_alpha, 2) {
|
|
373
|
-
fhold;
|
|
374
|
-
fnext *(in_set? ? fentry(character_set) : fentry(main));
|
|
375
|
-
fcall unicode_property;
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
(any -- non_literal_escape) | utf8_multibyte > (escaped_alpha, 1) {
|
|
379
|
-
emit(:escape, :literal, copy(data, ts-1, te))
|
|
380
|
-
fret;
|
|
381
|
-
};
|
|
382
|
-
*|;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# conditional expressions scanner
|
|
386
|
-
# --------------------------------------------------------------------------
|
|
387
|
-
conditional_expression := |*
|
|
388
|
-
group_lookup . ')' {
|
|
389
|
-
text = copy(data, ts, te-1)
|
|
390
|
-
text =~ /[^0]/ or raise ValidationError.for(:backref, 'condition', 'invalid ref ID')
|
|
391
|
-
emit(:conditional, :condition, text)
|
|
392
|
-
emit(:conditional, :condition_close, ')')
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
any {
|
|
396
|
-
fhold;
|
|
397
|
-
fcall main;
|
|
398
|
-
};
|
|
399
|
-
*|;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
# Main scanner
|
|
403
|
-
# --------------------------------------------------------------------------
|
|
404
|
-
main := |*
|
|
405
|
-
|
|
406
|
-
# Meta characters
|
|
407
|
-
# ------------------------------------------------------------------------
|
|
408
|
-
dot {
|
|
409
|
-
emit(:meta, :dot, copy(data, ts, te))
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
alternation {
|
|
413
|
-
if conditional_stack.last == group_depth
|
|
414
|
-
emit(:conditional, :separator, copy(data, ts, te))
|
|
415
|
-
else
|
|
416
|
-
emit(:meta, :alternation, copy(data, ts, te))
|
|
417
|
-
end
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
# Anchors
|
|
421
|
-
# ------------------------------------------------------------------------
|
|
422
|
-
beginning_of_line {
|
|
423
|
-
emit(:anchor, :bol, copy(data, ts, te))
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
end_of_line {
|
|
427
|
-
emit(:anchor, :eol, copy(data, ts, te))
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
backslash . keep_mark > (backslashed, 4) {
|
|
431
|
-
emit(:keep, :mark, copy(data, ts, te))
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
backslash . anchor_char > (backslashed, 3) {
|
|
435
|
-
case text = copy(data, ts, te)
|
|
436
|
-
when '\A'; emit(:anchor, :bos, text)
|
|
437
|
-
when '\z'; emit(:anchor, :eos, text)
|
|
438
|
-
when '\Z'; emit(:anchor, :eos_ob_eol, text)
|
|
439
|
-
when '\b'; emit(:anchor, :word_boundary, text)
|
|
440
|
-
when '\B'; emit(:anchor, :nonword_boundary, text)
|
|
441
|
-
when '\G'; emit(:anchor, :match_start, text)
|
|
442
|
-
end
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
literal_delimiters {
|
|
446
|
-
append_literal(data, ts, te)
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
# Character sets
|
|
450
|
-
# ------------------------------------------------------------------------
|
|
451
|
-
set_open >set_opened {
|
|
452
|
-
emit(:set, :open, copy(data, ts, te))
|
|
453
|
-
fcall character_set;
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
# Conditional expression
|
|
458
|
-
# (?(condition)Y|N) conditional expression
|
|
459
|
-
# ------------------------------------------------------------------------
|
|
460
|
-
conditional {
|
|
461
|
-
text = copy(data, ts, te)
|
|
462
|
-
|
|
463
|
-
conditional_stack << group_depth
|
|
464
|
-
|
|
465
|
-
emit(:conditional, :open, text[0..-2])
|
|
466
|
-
emit(:conditional, :condition_open, '(')
|
|
467
|
-
fcall conditional_expression;
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
# (?#...) comments: parsed as a single expression, without introducing a
|
|
472
|
-
# new nesting level. Comments may not include parentheses, escaped or not.
|
|
473
|
-
# special case for close to get the correct closing count.
|
|
474
|
-
# ------------------------------------------------------------------------
|
|
475
|
-
(group_open . group_comment) @group_closed {
|
|
476
|
-
emit(:group, :comment, copy(data, ts, te))
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
# Expression options:
|
|
480
|
-
# (?imxdau-imx) option on/off
|
|
481
|
-
# i: ignore case
|
|
482
|
-
# m: multi-line (dot(.) match newline)
|
|
483
|
-
# x: extended form
|
|
484
|
-
# d: default class rules (1.9 compatible)
|
|
485
|
-
# a: ASCII class rules (\s, \w, etc.)
|
|
486
|
-
# u: Unicode class rules (\s, \w, etc.)
|
|
487
|
-
#
|
|
488
|
-
# (?imxdau-imx:subexp) option on/off for subexp
|
|
489
|
-
# ------------------------------------------------------------------------
|
|
490
|
-
(group_open . group_options) >group_opened {
|
|
491
|
-
text = copy(data, ts, te)
|
|
492
|
-
if text[2..-1] =~ /([^\-mixdau:]|^$)|-.*([dau])/
|
|
493
|
-
raise ValidationError.for(:group_option, $1 || "-#{$2}", text)
|
|
494
|
-
end
|
|
495
|
-
emit_options(text)
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
# Assertions
|
|
499
|
-
# (?=subexp) look-ahead
|
|
500
|
-
# (?!subexp) negative look-ahead
|
|
501
|
-
# (?<=subexp) look-behind
|
|
502
|
-
# (?<!subexp) negative look-behind
|
|
503
|
-
# ------------------------------------------------------------------------
|
|
504
|
-
(group_open . assertion_type) >group_opened {
|
|
505
|
-
case text = copy(data, ts, te)
|
|
506
|
-
when '(?='; emit(:assertion, :lookahead, text)
|
|
507
|
-
when '(?!'; emit(:assertion, :nlookahead, text)
|
|
508
|
-
when '(?<='; emit(:assertion, :lookbehind, text)
|
|
509
|
-
when '(?<!'; emit(:assertion, :nlookbehind, text)
|
|
510
|
-
end
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
# Groups
|
|
514
|
-
# (?:subexp) passive (non-captured) group
|
|
515
|
-
# (?>subexp) atomic group, don't backtrack in subexp.
|
|
516
|
-
# (?~subexp) absence group, matches anything that is not subexp
|
|
517
|
-
# (?<name>subexp) named group
|
|
518
|
-
# (?'name'subexp) named group (single quoted version)
|
|
519
|
-
# (subexp) captured group
|
|
520
|
-
# ------------------------------------------------------------------------
|
|
521
|
-
(group_open . group_type) >group_opened {
|
|
522
|
-
case text = copy(data, ts, te)
|
|
523
|
-
when '(?:'; emit(:group, :passive, text)
|
|
524
|
-
when '(?>'; emit(:group, :atomic, text)
|
|
525
|
-
when '(?~'; emit(:group, :absence, text)
|
|
526
|
-
|
|
527
|
-
when /^\(\?(?:<>|'')/
|
|
528
|
-
raise ValidationError.for(:group, 'named group', 'name is empty')
|
|
529
|
-
|
|
530
|
-
when /^\(\?<[^>]+>/
|
|
531
|
-
emit(:group, :named_ab, text)
|
|
532
|
-
|
|
533
|
-
when /^\(\?'[^']+'/
|
|
534
|
-
emit(:group, :named_sq, text)
|
|
535
|
-
|
|
536
|
-
end
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
group_open @group_opened {
|
|
540
|
-
self.capturing_group_count = capturing_group_count + 1
|
|
541
|
-
text = copy(data, ts, te)
|
|
542
|
-
emit(:group, :capture, text)
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
group_close @group_closed {
|
|
546
|
-
if conditional_stack.last == group_depth + 1
|
|
547
|
-
conditional_stack.pop
|
|
548
|
-
emit(:conditional, :close, ')')
|
|
549
|
-
elsif group_depth >= 0
|
|
550
|
-
if spacing_stack.length > 1 &&
|
|
551
|
-
spacing_stack.last[:depth] == group_depth + 1
|
|
552
|
-
spacing_stack.pop
|
|
553
|
-
self.free_spacing = spacing_stack.last[:free_spacing]
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
emit(:group, :close, ')')
|
|
557
|
-
else
|
|
558
|
-
raise ValidationError.for(:group, 'group', 'unmatched close parenthesis')
|
|
559
|
-
end
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
# Group backreference, named and numbered
|
|
564
|
-
# ------------------------------------------------------------------------
|
|
565
|
-
backslash . (group_ref) > (backslashed, 4) {
|
|
566
|
-
case text = copy(data, ts, te)
|
|
567
|
-
when /^\\k(.)[^0-9\-][^+\-]*['>]$/
|
|
568
|
-
emit(:backref, $1 == '<' ? :name_ref_ab : :name_ref_sq, text)
|
|
569
|
-
when /^\\k(.)0*[1-9]\d*['>]$/
|
|
570
|
-
emit(:backref, $1 == '<' ? :number_ref_ab : :number_ref_sq, text)
|
|
571
|
-
when /^\\k(.)-0*[1-9]\d*['>]$/
|
|
572
|
-
emit(:backref, $1 == '<' ? :number_rel_ref_ab : :number_rel_ref_sq, text)
|
|
573
|
-
when /^\\k(.)[^0-9\-].*[+\-]\d+['>]$/
|
|
574
|
-
emit(:backref, $1 == '<' ? :name_recursion_ref_ab : :name_recursion_ref_sq, text)
|
|
575
|
-
when /^\\k(.)-?0*[1-9]\d*[+\-]\d+['>]$/
|
|
576
|
-
emit(:backref, $1 == '<' ? :number_recursion_ref_ab : :number_recursion_ref_sq, text)
|
|
577
|
-
else
|
|
578
|
-
raise ValidationError.for(:backref, 'backreference', 'invalid ref ID')
|
|
579
|
-
end
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
# Group call, named and numbered
|
|
583
|
-
# ------------------------------------------------------------------------
|
|
584
|
-
backslash . (group_call) > (backslashed, 4) {
|
|
585
|
-
case text = copy(data, ts, te)
|
|
586
|
-
when /^\\g(.)[^0-9+\-].*['>]$/
|
|
587
|
-
emit(:backref, $1 == '<' ? :name_call_ab : :name_call_sq, text)
|
|
588
|
-
when /^\\g(.)(?:0|0*[1-9]\d*)['>]$/
|
|
589
|
-
emit(:backref, $1 == '<' ? :number_call_ab : :number_call_sq, text)
|
|
590
|
-
when /^\\g(.)[+-]0*[1-9]\d*/
|
|
591
|
-
emit(:backref, $1 == '<' ? :number_rel_call_ab : :number_rel_call_sq, text)
|
|
592
|
-
else
|
|
593
|
-
raise ValidationError.for(:backref, 'subexpression call', 'invalid ref ID')
|
|
594
|
-
end
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# Quantifiers
|
|
599
|
-
# ------------------------------------------------------------------------
|
|
600
|
-
zero_or_one {
|
|
601
|
-
case text = copy(data, ts, te)
|
|
602
|
-
when '?' ; emit(:quantifier, :zero_or_one, text)
|
|
603
|
-
when '??'; emit(:quantifier, :zero_or_one_reluctant, text)
|
|
604
|
-
when '?+'; emit(:quantifier, :zero_or_one_possessive, text)
|
|
605
|
-
end
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
zero_or_more {
|
|
609
|
-
case text = copy(data, ts, te)
|
|
610
|
-
when '*' ; emit(:quantifier, :zero_or_more, text)
|
|
611
|
-
when '*?'; emit(:quantifier, :zero_or_more_reluctant, text)
|
|
612
|
-
when '*+'; emit(:quantifier, :zero_or_more_possessive, text)
|
|
613
|
-
end
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
one_or_more {
|
|
617
|
-
case text = copy(data, ts, te)
|
|
618
|
-
when '+' ; emit(:quantifier, :one_or_more, text)
|
|
619
|
-
when '+?'; emit(:quantifier, :one_or_more_reluctant, text)
|
|
620
|
-
when '++'; emit(:quantifier, :one_or_more_possessive, text)
|
|
621
|
-
end
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
quantifier_interval {
|
|
625
|
-
emit(:quantifier, :interval, copy(data, ts, te))
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
# Catch unmatched curly braces as literals
|
|
629
|
-
range_open {
|
|
630
|
-
append_literal(data, ts, te)
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
# Escaped sequences
|
|
634
|
-
# ------------------------------------------------------------------------
|
|
635
|
-
backslash > (backslashed, 1) {
|
|
636
|
-
fcall escape_sequence;
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
comment {
|
|
640
|
-
if free_spacing
|
|
641
|
-
emit(:free_space, :comment, copy(data, ts, te))
|
|
642
|
-
else
|
|
643
|
-
# consume only the pound sign (#) and backtrack to do regular scanning
|
|
644
|
-
append_literal(data, ts, ts + 1)
|
|
645
|
-
fexec ts + 1;
|
|
646
|
-
end
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
space+ {
|
|
650
|
-
if free_spacing
|
|
651
|
-
emit(:free_space, :whitespace, copy(data, ts, te))
|
|
652
|
-
else
|
|
653
|
-
append_literal(data, ts, te)
|
|
654
|
-
end
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
# Literal: any run of ASCII (pritable or non-printable), and/or UTF-8,
|
|
658
|
-
# except meta characters.
|
|
659
|
-
# ------------------------------------------------------------------------
|
|
660
|
-
(ascii_print -- space)+ | ascii_nonprint+ | utf8_multibyte+ {
|
|
661
|
-
append_literal(data, ts, te)
|
|
662
|
-
};
|
|
663
|
-
|
|
664
|
-
*|;
|
|
665
|
-
}%%
|
|
666
|
-
|
|
667
|
-
require_relative 'scanner/errors/scanner_error'
|
|
668
|
-
require_relative 'scanner/errors/premature_end_error'
|
|
669
|
-
require_relative 'scanner/errors/validation_error'
|
|
670
|
-
|
|
671
|
-
class Regexp::Scanner
|
|
672
|
-
# Scans the given regular expression text, or Regexp object and collects the
|
|
673
|
-
# emitted token into an array that gets returned at the end. If a block is
|
|
674
|
-
# given, it gets called for each emitted token.
|
|
675
|
-
#
|
|
676
|
-
# This method may raise errors if a syntax error is encountered.
|
|
677
|
-
# --------------------------------------------------------------------------
|
|
678
|
-
def self.scan(input_object, options: nil, collect_tokens: true, &block)
|
|
679
|
-
new.scan(input_object, options: options, collect_tokens: collect_tokens, &block)
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
def scan(input_object, options: nil, collect_tokens: true, &block)
|
|
683
|
-
self.collect_tokens = collect_tokens
|
|
684
|
-
self.literal_run = nil
|
|
685
|
-
stack = []
|
|
686
|
-
|
|
687
|
-
input = input_object.is_a?(Regexp) ? input_object.source : input_object
|
|
688
|
-
self.free_spacing = free_spacing?(input_object, options)
|
|
689
|
-
self.regexp_encoding = extract_encoding(input_object, options)
|
|
690
|
-
self.spacing_stack = [{:free_spacing => free_spacing, :depth => 0}]
|
|
691
|
-
|
|
692
|
-
data = input.unpack("c*")
|
|
693
|
-
eof = data.length
|
|
694
|
-
|
|
695
|
-
self.tokens = []
|
|
696
|
-
self.block = block
|
|
697
|
-
|
|
698
|
-
self.set_depth = 0
|
|
699
|
-
self.group_depth = 0
|
|
700
|
-
self.capturing_group_count = 0
|
|
701
|
-
self.conditional_stack = []
|
|
702
|
-
self.char_pos = 0
|
|
703
|
-
|
|
704
|
-
%% write data;
|
|
705
|
-
%% write init;
|
|
706
|
-
%% write exec;
|
|
707
|
-
|
|
708
|
-
# to avoid "warning: assigned but unused variable - testEof"
|
|
709
|
-
testEof = testEof
|
|
710
|
-
|
|
711
|
-
if cs == re_scanner_error
|
|
712
|
-
text = copy(data, ts ? ts-1 : 0, -1)
|
|
713
|
-
raise ScannerError.new("Scan error at '#{text}'")
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
raise PrematureEndError.new("(missing group closing paranthesis) "+
|
|
717
|
-
"[#{group_depth}]") if in_group?
|
|
718
|
-
raise PrematureEndError.new("(missing set closing bracket) "+
|
|
719
|
-
"[#{set_depth}]") if in_set?
|
|
720
|
-
|
|
721
|
-
# when the entire expression is a literal run
|
|
722
|
-
emit_literal if literal_run
|
|
723
|
-
|
|
724
|
-
tokens
|
|
725
|
-
end
|
|
726
|
-
|
|
727
|
-
# lazy-load property maps when first needed
|
|
728
|
-
def self.short_prop_map
|
|
729
|
-
@short_prop_map ||= parse_prop_map('short')
|
|
730
|
-
end
|
|
731
|
-
|
|
732
|
-
def self.long_prop_map
|
|
733
|
-
@long_prop_map ||= parse_prop_map('long')
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
def self.parse_prop_map(name)
|
|
737
|
-
File.read("#{__dir__}/scanner/properties/#{name}.csv").scan(/(.+),(.+)/).to_h
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
# Use each_with_object for required_ruby_version >= 2.2, or #to_h for >= 2.6
|
|
741
|
-
POSIX_CLASSES =
|
|
742
|
-
%w[alnum alpha ascii blank cntrl digit graph
|
|
743
|
-
lower print punct space upper word xdigit]
|
|
744
|
-
.inject({}) { |o, e| o.merge(e => true) }.freeze
|
|
745
|
-
|
|
746
|
-
# Emits an array with the details of the scanned pattern
|
|
747
|
-
def emit(type, token, text)
|
|
748
|
-
#puts "EMIT: type: #{type}, token: #{token}, text: #{text}, ts: #{ts}, te: #{te}"
|
|
749
|
-
|
|
750
|
-
emit_literal if literal_run
|
|
751
|
-
|
|
752
|
-
# Ragel runs with byte-based indices (ts, te). These are of little value to
|
|
753
|
-
# end-users, so we keep track of char-based indices and emit those instead.
|
|
754
|
-
ts_char_pos = char_pos
|
|
755
|
-
te_char_pos = char_pos + text.length
|
|
756
|
-
|
|
757
|
-
tok = [type, token, text, ts_char_pos, te_char_pos]
|
|
758
|
-
|
|
759
|
-
self.prev_token = tok
|
|
760
|
-
|
|
761
|
-
self.char_pos = te_char_pos
|
|
762
|
-
|
|
763
|
-
if block
|
|
764
|
-
block.call type, token, text, ts_char_pos, te_char_pos
|
|
765
|
-
# TODO: in v3.0.0, remove `collect_tokens:` kwarg and only collect if no block given
|
|
766
|
-
tokens << tok if collect_tokens
|
|
767
|
-
elsif collect_tokens
|
|
768
|
-
tokens << tok
|
|
769
|
-
end
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
attr_accessor :capturing_group_count, :literal_run # only public for #||= to work on ruby <= 2.5
|
|
773
|
-
|
|
774
|
-
private
|
|
775
|
-
|
|
776
|
-
attr_accessor :block,
|
|
777
|
-
:collect_tokens, :tokens, :prev_token,
|
|
778
|
-
:free_spacing, :spacing_stack,
|
|
779
|
-
:regexp_encoding,
|
|
780
|
-
:group_depth, :set_depth, :conditional_stack,
|
|
781
|
-
:char_pos
|
|
782
|
-
|
|
783
|
-
def extract_encoding(input_object, options)
|
|
784
|
-
if input_object.is_a?(::Regexp)
|
|
785
|
-
input_object.encoding
|
|
786
|
-
elsif options && (options & Regexp::NOENCODING)
|
|
787
|
-
Encoding::BINARY
|
|
788
|
-
end
|
|
789
|
-
end
|
|
790
|
-
|
|
791
|
-
def free_spacing?(input_object, options)
|
|
792
|
-
if options && !input_object.is_a?(String)
|
|
793
|
-
raise ArgumentError, 'options cannot be supplied unless scanning a String'
|
|
794
|
-
end
|
|
795
|
-
|
|
796
|
-
options = input_object.options if input_object.is_a?(::Regexp)
|
|
797
|
-
|
|
798
|
-
return false unless options
|
|
799
|
-
|
|
800
|
-
options & Regexp::EXTENDED != 0
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
def in_group?
|
|
804
|
-
group_depth > 0
|
|
805
|
-
end
|
|
806
|
-
|
|
807
|
-
def in_set?
|
|
808
|
-
set_depth > 0
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
# Copy from ts to te from data as text
|
|
812
|
-
def copy(data, ts, te)
|
|
813
|
-
data[ts...te].pack('c*').force_encoding('utf-8')
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
# Appends one or more characters to the literal buffer, to be emitted later
|
|
817
|
-
# by a call to emit_literal.
|
|
818
|
-
def append_literal(data, ts, te)
|
|
819
|
-
(self.literal_run ||= []) << copy(data, ts, te)
|
|
820
|
-
end
|
|
821
|
-
|
|
822
|
-
# Emits the literal run collected by calls to the append_literal method.
|
|
823
|
-
def emit_literal
|
|
824
|
-
text = literal_run.join
|
|
825
|
-
self.literal_run = nil
|
|
826
|
-
emit(:literal, :literal, text)
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
def emit_options(text)
|
|
830
|
-
token = nil
|
|
831
|
-
|
|
832
|
-
# Ruby allows things like '(?-xxxx)' or '(?xx-xx--xx-:abc)'.
|
|
833
|
-
text =~ /\(\?([mixdau]*)(-(?:[mix]*))*(:)?/
|
|
834
|
-
positive, negative, group_local = $1, $2, $3
|
|
835
|
-
|
|
836
|
-
if positive.include?('x')
|
|
837
|
-
self.free_spacing = true
|
|
838
|
-
end
|
|
839
|
-
|
|
840
|
-
# If the x appears in both, treat it like ruby does, the second cancels
|
|
841
|
-
# the first.
|
|
842
|
-
if negative && negative.include?('x')
|
|
843
|
-
self.free_spacing = false
|
|
844
|
-
end
|
|
845
|
-
|
|
846
|
-
if group_local
|
|
847
|
-
spacing_stack << {:free_spacing => free_spacing, :depth => group_depth}
|
|
848
|
-
token = :options
|
|
849
|
-
else
|
|
850
|
-
# switch for parent group level
|
|
851
|
-
spacing_stack.last[:free_spacing] = free_spacing
|
|
852
|
-
token = :options_switch
|
|
853
|
-
end
|
|
854
|
-
|
|
855
|
-
emit(:group, token, text)
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
def emit_meta_control_sequence(data, ts, te, token)
|
|
859
|
-
if data.last < 0x00 || data.last > 0x7F
|
|
860
|
-
raise ValidationError.for(:sequence, 'escape', token.to_s)
|
|
861
|
-
end
|
|
862
|
-
emit(:escape, token, copy(data, ts-1, te))
|
|
863
|
-
end
|
|
864
|
-
end # module Regexp::Scanner
|