regex-inspector 1.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.
package/dist/parser.js ADDED
@@ -0,0 +1,776 @@
1
+ import { digits, dot, whitespace, wordChars } from "./preset.js";
2
+ // ── Constants ─────────────────────────────────────────────────────────────
3
+ const MAX_PATTERN_LENGTH = 100_000;
4
+ const CAPTURE_NAME_FIRST = /^[a-zA-Z_$]$/;
5
+ const CAPTURE_NAME_CHAR = /^[a-zA-Z0-9_$]$/;
6
+ const OCTAL_DIGIT = /^[0-7]$/;
7
+ const DIGIT = /^\d$/;
8
+ // ── Errors ────────────────────────────────────────────────────────────────
9
+ class ParseError extends SyntaxError {
10
+ constructor(pattern, message, position) {
11
+ const truncated = pattern.length > 60 ? `${pattern.slice(0, 60)}...` : pattern;
12
+ const pos = position !== undefined ? ` at column ${position}` : "";
13
+ super(`Invalid regular expression: /${truncated}/: ${message}${pos}`);
14
+ this.name = "ParseError";
15
+ }
16
+ }
17
+ // ── Escape resolution ─────────────────────────────────────────────────────
18
+ function resolveEscapes(source) {
19
+ // Matches any character after \c -- V8 accepts any code unit;
20
+ // the control code is codepoint % 32.
21
+ const escRe = /(\[\\b\])|(\\)?\\(?:u\{([A-Fa-f0-9]{1,6})\}|u([A-Fa-f0-9]{4})|x([A-Fa-f0-9]{2})|c([\s\S])|(0(?!\d)|[tnvfr]))/g;
22
+ const ctrlTable = {
23
+ "@": 0,
24
+ A: 1,
25
+ B: 2,
26
+ C: 3,
27
+ D: 4,
28
+ E: 5,
29
+ F: 6,
30
+ G: 7,
31
+ H: 8,
32
+ I: 9,
33
+ J: 10,
34
+ K: 11,
35
+ L: 12,
36
+ M: 13,
37
+ N: 14,
38
+ O: 15,
39
+ P: 16,
40
+ Q: 17,
41
+ R: 18,
42
+ S: 19,
43
+ T: 20,
44
+ U: 21,
45
+ V: 22,
46
+ W: 23,
47
+ X: 24,
48
+ Y: 25,
49
+ Z: 26,
50
+ "[": 27,
51
+ "\\": 28,
52
+ "]": 29,
53
+ "^": 30,
54
+ "?": 31,
55
+ };
56
+ const simpleEscapes = {
57
+ "0": 0,
58
+ t: 9,
59
+ n: 10,
60
+ v: 11,
61
+ f: 12,
62
+ r: 13,
63
+ };
64
+ return source.replace(escRe, (match, _bs, literalBackslash, ubrace, u4, x2, ctrl, simple) => {
65
+ if (literalBackslash)
66
+ return match;
67
+ if (_bs)
68
+ return "\u0008";
69
+ let code;
70
+ if (ubrace) {
71
+ code = parseInt(ubrace, 16);
72
+ if (code > 0x10ffff)
73
+ throw new ParseError(source, "Invalid Unicode escape");
74
+ }
75
+ else if (u4) {
76
+ code = parseInt(u4, 16);
77
+ }
78
+ else if (x2) {
79
+ code = parseInt(x2, 16);
80
+ }
81
+ else if (ctrl) {
82
+ code = ctrlTable[ctrl] ?? ctrl.codePointAt(0) % 32;
83
+ }
84
+ else {
85
+ code = simpleEscapes[simple] ?? 0;
86
+ }
87
+ try {
88
+ const c = String.fromCodePoint(code);
89
+ return /[[\]{}^$.|?*+()\\]/.test(c) ? `\\${c}` : c;
90
+ }
91
+ catch {
92
+ return String.fromCharCode(code);
93
+ }
94
+ });
95
+ }
96
+ // ── Tokenizer ─────────────────────────────────────────────────────────────
97
+ export function tokenize(pattern) {
98
+ if (pattern.length > MAX_PATTERN_LENGTH) {
99
+ throw new ParseError(pattern, "Regular expression too large");
100
+ }
101
+ const resolved = resolveEscapes(pattern);
102
+ const chars = [...resolved];
103
+ let pos = 0;
104
+ const root = { kind: "root", branches: [[]] };
105
+ const groupStack = [];
106
+ // Returns the branch that new tokens should be appended to.
107
+ function currentBranch() {
108
+ const g = groupStack.length > 0 ? groupStack[groupStack.length - 1] : root;
109
+ return g.branches[g.branches.length - 1];
110
+ }
111
+ const backrefs = [];
112
+ const namedBackrefs = [];
113
+ const nameToIndex = new Map();
114
+ let groupCount = 0;
115
+ function peek(offset = 0) {
116
+ return chars[pos + offset];
117
+ }
118
+ function advance() {
119
+ return chars[pos++];
120
+ }
121
+ function atEnd() {
122
+ return pos >= chars.length;
123
+ }
124
+ // ── Character class parsing ───────────────────────────────────────
125
+ /**
126
+ * Post-processes a flat member list to detect -- (subtract) and &&
127
+ * (intersect) operators and builds the set op tree.
128
+ * && binds tighter than --; both are left-associative.
129
+ */
130
+ function buildSetOpTree(members) {
131
+ const items = [];
132
+ for (const m of members) {
133
+ if (m._op) {
134
+ items.push(m);
135
+ }
136
+ else {
137
+ items.push(m);
138
+ }
139
+ }
140
+ // Phase 2: build tree. && binds tighter → group && first.
141
+ return buildOps(items, "intersect");
142
+ }
143
+ function isOp(item, op) {
144
+ return item._op === op;
145
+ }
146
+ /** Left-associative grouping of `items` by `op`. */
147
+ function buildOps(items, op) {
148
+ // First pass: combine with the given operator
149
+ const result = [];
150
+ let i = 0;
151
+ while (i < items.length) {
152
+ const item = items[i];
153
+ if (isOp(item, op)) {
154
+ const left = result.pop();
155
+ i++;
156
+ if (i >= items.length) {
157
+ result.push(left);
158
+ break;
159
+ }
160
+ const right = items[i];
161
+ if (isOp(right, "subtract") || isOp(right, "intersect")) {
162
+ result.push(left);
163
+ result.push(right);
164
+ }
165
+ else {
166
+ result.push({
167
+ kind: "set_op",
168
+ operator: op,
169
+ left: left,
170
+ right: right,
171
+ });
172
+ }
173
+ i++;
174
+ }
175
+ else {
176
+ result.push(item);
177
+ i++;
178
+ }
179
+ }
180
+ // If we just handled intersects, now handle subtracts (lower precedence)
181
+ if (op === "intersect") {
182
+ return buildOps(result, "subtract");
183
+ }
184
+ return result;
185
+ }
186
+ function parseCharClass() {
187
+ let negated = false;
188
+ if (!atEnd() && peek() === "^") {
189
+ negated = true;
190
+ advance();
191
+ }
192
+ const members = [];
193
+ let rangeStart = null;
194
+ let awaitingRangeEnd = false;
195
+ let foundClosing = false;
196
+ let hasSetOps = false;
197
+ let prevWasEscapedDash = false;
198
+ function flushPending() {
199
+ if (rangeStart !== null) {
200
+ members.push({ kind: "char", value: rangeStart });
201
+ rangeStart = null;
202
+ }
203
+ awaitingRangeEnd = false;
204
+ }
205
+ while (!atEnd()) {
206
+ const c = advance();
207
+ if (c === "]") {
208
+ foundClosing = true;
209
+ const hadPendingRange = awaitingRangeEnd;
210
+ flushPending();
211
+ if (hadPendingRange) {
212
+ // Trailing dash before ]: literal hyphen
213
+ members.push({ kind: "char", value: 45 });
214
+ }
215
+ break;
216
+ }
217
+ if (c === "\\") {
218
+ if (atEnd())
219
+ throw new ParseError(pattern, "Unterminated character class");
220
+ const esc = advance();
221
+ // Helper: add a literal escape result (the actual code point)
222
+ const addEscapedChar = (code) => {
223
+ // Code points > 0xFFFF must be split into surrogate pairs inside classes
224
+ if (code > 0xffff) {
225
+ const hi = Math.floor((code - 0x10000) / 0x400) + 0xd800;
226
+ const lo = ((code - 0x10000) % 0x400) + 0xdc00;
227
+ addEscapedChar(hi);
228
+ addEscapedChar(lo);
229
+ return;
230
+ }
231
+ if (awaitingRangeEnd && rangeStart !== null) {
232
+ if (rangeStart > code)
233
+ throw new ParseError(pattern, "Range out of order in character class");
234
+ members.push({ kind: "range", from: rangeStart, to: code });
235
+ rangeStart = null;
236
+ awaitingRangeEnd = false;
237
+ }
238
+ else {
239
+ flushPending();
240
+ rangeStart = code;
241
+ }
242
+ if (code === 45)
243
+ prevWasEscapedDash = true;
244
+ };
245
+ switch (esc) {
246
+ case "d":
247
+ flushPending();
248
+ members.push(digits());
249
+ break;
250
+ case "D":
251
+ flushPending();
252
+ members.push({ ...digits(), negated: true });
253
+ break;
254
+ case "w":
255
+ flushPending();
256
+ members.push(wordChars());
257
+ break;
258
+ case "W":
259
+ flushPending();
260
+ members.push({ ...wordChars(), negated: true });
261
+ break;
262
+ case "s":
263
+ flushPending();
264
+ members.push(whitespace());
265
+ break;
266
+ case "S":
267
+ flushPending();
268
+ members.push({ ...whitespace(), negated: true });
269
+ break;
270
+ case "p":
271
+ case "P": {
272
+ flushPending();
273
+ if (!atEnd() && peek() === "{") {
274
+ advance();
275
+ let propName = "";
276
+ while (!atEnd() && peek() !== "}")
277
+ propName += advance();
278
+ if (!atEnd())
279
+ advance();
280
+ members.push({
281
+ kind: "unicode_property",
282
+ property: propName,
283
+ negated: esc === "P",
284
+ });
285
+ }
286
+ else {
287
+ addEscapedChar(esc.codePointAt(0));
288
+ }
289
+ break;
290
+ }
291
+ case "q": {
292
+ flushPending();
293
+ if (!atEnd() && peek() === "{") {
294
+ advance();
295
+ const strings = [[]];
296
+ while (!atEnd() && peek() !== "}") {
297
+ const ch = advance();
298
+ if (ch === "|") {
299
+ strings.push([]);
300
+ }
301
+ else {
302
+ strings[strings.length - 1].push(ch.codePointAt(0));
303
+ }
304
+ }
305
+ // Drop trailing empty string if | was the last char
306
+ if (strings.length > 0 &&
307
+ strings[strings.length - 1].length === 0) {
308
+ strings.pop();
309
+ }
310
+ if (!atEnd())
311
+ advance();
312
+ members.push({ kind: "string_member", strings, negated: false });
313
+ }
314
+ break;
315
+ }
316
+ case "b":
317
+ addEscapedChar(8);
318
+ break;
319
+ default:
320
+ addEscapedChar(esc.codePointAt(0));
321
+ }
322
+ continue;
323
+ }
324
+ if ((c === "-" || c === "&") && !atEnd() && peek() === c) {
325
+ if (c === "-" && prevWasEscapedDash) {
326
+ prevWasEscapedDash = false;
327
+ if (rangeStart !== null) {
328
+ awaitingRangeEnd = true;
329
+ }
330
+ else {
331
+ members.push({ kind: "char", value: 45 });
332
+ }
333
+ continue;
334
+ }
335
+ flushPending();
336
+ advance();
337
+ members.push({ _op: c === "-" ? "subtract" : "intersect" });
338
+ hasSetOps = true;
339
+ prevWasEscapedDash = false;
340
+ continue;
341
+ }
342
+ prevWasEscapedDash = false;
343
+ // Nested character class: only in v-mode set-op context
344
+ if (c === "[" && hasSetOps) {
345
+ flushPending();
346
+ members.push(parseCharClass());
347
+ continue;
348
+ }
349
+ if (c === "-") {
350
+ if (rangeStart !== null) {
351
+ awaitingRangeEnd = true;
352
+ }
353
+ else {
354
+ members.push({ kind: "char", value: 45 });
355
+ }
356
+ continue;
357
+ }
358
+ // Regular character
359
+ const code = c.codePointAt(0);
360
+ if (code > 0xffff) {
361
+ // Split into surrogate pair
362
+ const hi = Math.floor((code - 0x10000) / 0x400) + 0xd800;
363
+ const lo = ((code - 0x10000) % 0x400) + 0xdc00;
364
+ for (const cp of [hi, lo]) {
365
+ if (awaitingRangeEnd && rangeStart !== null) {
366
+ if (rangeStart > cp)
367
+ throw new ParseError(pattern, "Range out of order in character class");
368
+ members.push({ kind: "range", from: rangeStart, to: cp });
369
+ rangeStart = null;
370
+ awaitingRangeEnd = false;
371
+ }
372
+ else {
373
+ flushPending();
374
+ rangeStart = cp;
375
+ }
376
+ }
377
+ }
378
+ else if (awaitingRangeEnd && rangeStart !== null) {
379
+ if (rangeStart > code) {
380
+ throw new ParseError(pattern, "Range out of order in character class");
381
+ }
382
+ members.push({ kind: "range", from: rangeStart, to: code });
383
+ rangeStart = null;
384
+ awaitingRangeEnd = false;
385
+ }
386
+ else {
387
+ flushPending();
388
+ rangeStart = code;
389
+ }
390
+ }
391
+ if (!foundClosing) {
392
+ throw new ParseError(pattern, "Unterminated character class");
393
+ }
394
+ flushPending();
395
+ // ── Set operation post-processing ───────────────────────────────
396
+ // Detect -- (subtract) and && (intersect) from adjacent char members
397
+ // and build the set op tree. && binds tighter than --.
398
+ const processed = buildSetOpTree(members);
399
+ return { kind: "charset", negated, members: processed };
400
+ }
401
+ // ── Quantifier parsing ────────────────────────────────────────────
402
+ function parseQuantifier() {
403
+ const startPos = pos;
404
+ let minStr = "";
405
+ while (!atEnd() && DIGIT.test(peek()))
406
+ minStr += advance();
407
+ if (minStr.length === 0) {
408
+ pos = startPos;
409
+ return null;
410
+ }
411
+ let maxStr = "";
412
+ let hasComma = false;
413
+ if (!atEnd() && peek() === ",") {
414
+ hasComma = true;
415
+ advance();
416
+ while (!atEnd() && DIGIT.test(peek()))
417
+ maxStr += advance();
418
+ }
419
+ if (atEnd() || peek() !== "}") {
420
+ pos = startPos;
421
+ return null;
422
+ }
423
+ advance();
424
+ const min = parseInt(minStr, 10);
425
+ let max;
426
+ if (maxStr.length > 0)
427
+ max = parseInt(maxStr, 10);
428
+ else if (hasComma)
429
+ max = Infinity;
430
+ else
431
+ max = min;
432
+ if (max !== Infinity && min > max) {
433
+ throw new ParseError(pattern, "Numbers out of order in {} quantifier");
434
+ }
435
+ return { min, max };
436
+ }
437
+ function applyQuantifier(min, max, column) {
438
+ const branch = currentBranch();
439
+ if (branch.length === 0) {
440
+ throw new ParseError(pattern, "Nothing to repeat", column);
441
+ }
442
+ const child = branch.pop();
443
+ let greedy = true;
444
+ if (!atEnd() && peek() === "?") {
445
+ greedy = false;
446
+ advance();
447
+ }
448
+ branch.push({ kind: "repetition", min, max, greedy, child });
449
+ }
450
+ // ── Main loop ─────────────────────────────────────────────────────
451
+ while (!atEnd()) {
452
+ const c = advance();
453
+ const col = pos;
454
+ const branch = currentBranch();
455
+ switch (c) {
456
+ case "\\": {
457
+ if (atEnd())
458
+ throw new ParseError(pattern, "\\ at end of pattern");
459
+ const esc = advance();
460
+ switch (esc) {
461
+ case "b":
462
+ branch.push({ kind: "position", value: "b" });
463
+ break;
464
+ case "B":
465
+ branch.push({ kind: "position", value: "B" });
466
+ break;
467
+ case "d":
468
+ branch.push(digits());
469
+ break;
470
+ case "D":
471
+ branch.push({ ...digits(), negated: true });
472
+ break;
473
+ case "w":
474
+ branch.push(wordChars());
475
+ break;
476
+ case "W":
477
+ branch.push({ ...wordChars(), negated: true });
478
+ break;
479
+ case "s":
480
+ branch.push(whitespace());
481
+ break;
482
+ case "S":
483
+ branch.push({ ...whitespace(), negated: true });
484
+ break;
485
+ case "p":
486
+ case "P": {
487
+ if (!atEnd() && peek() === "{") {
488
+ advance();
489
+ let propName = "";
490
+ while (!atEnd() && peek() !== "}")
491
+ propName += advance();
492
+ if (!atEnd())
493
+ advance();
494
+ branch.push({
495
+ kind: "unicode_property",
496
+ property: propName,
497
+ negated: esc === "P",
498
+ });
499
+ }
500
+ else {
501
+ branch.push({ kind: "char", value: esc.codePointAt(0) });
502
+ }
503
+ break;
504
+ }
505
+ case "k": {
506
+ if (!atEnd() && peek() === "<") {
507
+ advance();
508
+ let name = "";
509
+ while (!atEnd() && peek() !== ">")
510
+ name += advance();
511
+ if (!atEnd())
512
+ advance();
513
+ const ref = {
514
+ kind: "backreference",
515
+ index: 0,
516
+ };
517
+ branch.push(ref);
518
+ namedBackrefs.push({
519
+ node: ref,
520
+ name,
521
+ branch,
522
+ index: branch.length - 1,
523
+ });
524
+ }
525
+ else {
526
+ branch.push({ kind: "char", value: 107 });
527
+ }
528
+ break;
529
+ }
530
+ default: {
531
+ if (DIGIT.test(esc)) {
532
+ if (esc === "0") {
533
+ let octal = esc;
534
+ let count = 0;
535
+ while (!atEnd() && OCTAL_DIGIT.test(peek()) && count < 2) {
536
+ octal += advance();
537
+ count++;
538
+ }
539
+ branch.push({ kind: "char", value: parseInt(octal, 8) });
540
+ }
541
+ else {
542
+ let digits = esc;
543
+ while (!atEnd() && DIGIT.test(peek()))
544
+ digits += advance();
545
+ const value = parseInt(digits, 10);
546
+ const ref = {
547
+ kind: "backreference",
548
+ index: value,
549
+ };
550
+ branch.push(ref);
551
+ backrefs.push({ node: ref, branch, index: branch.length - 1 });
552
+ }
553
+ }
554
+ else {
555
+ branch.push({ kind: "char", value: esc.codePointAt(0) });
556
+ }
557
+ }
558
+ }
559
+ break;
560
+ }
561
+ case "^":
562
+ branch.push({ kind: "position", value: "^" });
563
+ break;
564
+ case "$":
565
+ branch.push({ kind: "position", value: "$" });
566
+ break;
567
+ case ".":
568
+ branch.push(dot());
569
+ break;
570
+ case "[":
571
+ branch.push(parseCharClass());
572
+ break;
573
+ case "(": {
574
+ const group = {
575
+ kind: "group",
576
+ capturing: true,
577
+ branches: [[]],
578
+ };
579
+ if (!atEnd() && peek() === "?") {
580
+ advance();
581
+ if (atEnd())
582
+ throw new ParseError(pattern, "Invalid group", pos);
583
+ const mod = advance();
584
+ group.capturing = false;
585
+ if (mod === ":") {
586
+ // non-capturing
587
+ }
588
+ else if (mod === "=") {
589
+ group.lookahead = true;
590
+ }
591
+ else if (mod === "!") {
592
+ group.negatedLookahead = true;
593
+ }
594
+ else if (mod === "<") {
595
+ if (atEnd())
596
+ throw new ParseError(pattern, "Invalid group", pos);
597
+ const next = peek();
598
+ if (next === "=") {
599
+ advance();
600
+ group.lookbehind = true;
601
+ }
602
+ else if (next === "!") {
603
+ advance();
604
+ group.negatedLookbehind = true;
605
+ }
606
+ else {
607
+ group.capturing = true;
608
+ let name = "";
609
+ if (!CAPTURE_NAME_FIRST.test(next)) {
610
+ throw new ParseError(pattern, `Invalid capture group name, character '${next}' after '<'`, pos);
611
+ }
612
+ name += advance();
613
+ while (!atEnd() && CAPTURE_NAME_CHAR.test(peek()))
614
+ name += advance();
615
+ if (name.length === 0)
616
+ throw new ParseError(pattern, "Invalid capture group name", pos);
617
+ if (atEnd() || peek() !== ">") {
618
+ throw new ParseError(pattern, "Unclosed capture group name, expected >", pos);
619
+ }
620
+ advance();
621
+ if (nameToIndex.has(name))
622
+ throw new ParseError(pattern, "Duplicate capture group name");
623
+ group.name = name;
624
+ }
625
+ }
626
+ else if (/[ims-]/.test(mod)) {
627
+ let flagStr = mod;
628
+ while (!atEnd() && /[ims-]/.test(peek()))
629
+ flagStr += advance();
630
+ group.modifiers = flagStr;
631
+ if (!atEnd() && peek() === ":")
632
+ advance();
633
+ }
634
+ else {
635
+ throw new ParseError(pattern, `Invalid group, character '${mod}' after '?'`, pos);
636
+ }
637
+ }
638
+ if (group.capturing) {
639
+ groupCount++;
640
+ if (group.name)
641
+ nameToIndex.set(group.name, groupCount);
642
+ }
643
+ branch.push(group);
644
+ groupStack.push(group);
645
+ break;
646
+ }
647
+ case ")": {
648
+ if (groupStack.length === 0) {
649
+ throw new ParseError(pattern, "Unmatched )", col);
650
+ }
651
+ groupStack.pop();
652
+ break;
653
+ }
654
+ case "|": {
655
+ const g = groupStack.length > 0 ? groupStack[groupStack.length - 1] : root;
656
+ g.branches.push([]);
657
+ break;
658
+ }
659
+ case "{": {
660
+ const q = parseQuantifier();
661
+ if (q)
662
+ applyQuantifier(q.min, q.max, col);
663
+ else
664
+ branch.push({ kind: "char", value: 123 });
665
+ break;
666
+ }
667
+ case "?": {
668
+ const prev = branch[branch.length - 1];
669
+ if (prev && prev.kind === "repetition" && prev.greedy) {
670
+ prev.greedy = false;
671
+ }
672
+ else if (prev && prev.kind === "repetition" && !prev.greedy) {
673
+ throw new ParseError(pattern, "Nothing to repeat", col);
674
+ }
675
+ else if (branch.length === 0) {
676
+ throw new ParseError(pattern, "Nothing to repeat", col);
677
+ }
678
+ else {
679
+ const child = branch.pop();
680
+ let greedy = true;
681
+ if (!atEnd() && peek() === "?") {
682
+ greedy = false;
683
+ advance();
684
+ }
685
+ branch.push({ kind: "repetition", min: 0, max: 1, greedy, child });
686
+ }
687
+ break;
688
+ }
689
+ case "*": {
690
+ if (branch.length === 0)
691
+ throw new ParseError(pattern, "Nothing to repeat", col);
692
+ const prev = branch[branch.length - 1];
693
+ if (prev.kind === "repetition")
694
+ throw new ParseError(pattern, "Nothing to repeat", col);
695
+ const child = branch.pop();
696
+ let greedy = true;
697
+ if (!atEnd() && peek() === "?") {
698
+ greedy = false;
699
+ advance();
700
+ }
701
+ branch.push({
702
+ kind: "repetition",
703
+ min: 0,
704
+ max: Infinity,
705
+ greedy,
706
+ child,
707
+ });
708
+ break;
709
+ }
710
+ case "+": {
711
+ if (branch.length === 0)
712
+ throw new ParseError(pattern, "Nothing to repeat", col);
713
+ const prev = branch[branch.length - 1];
714
+ if (prev.kind === "repetition")
715
+ throw new ParseError(pattern, "Nothing to repeat", col);
716
+ const child = branch.pop();
717
+ let greedy = true;
718
+ if (!atEnd() && peek() === "?") {
719
+ greedy = false;
720
+ advance();
721
+ }
722
+ branch.push({
723
+ kind: "repetition",
724
+ min: 1,
725
+ max: Infinity,
726
+ greedy,
727
+ child,
728
+ });
729
+ break;
730
+ }
731
+ default:
732
+ branch.push({ kind: "char", value: c.codePointAt(0) });
733
+ }
734
+ }
735
+ if (groupStack.length > 0) {
736
+ throw new ParseError(pattern, "Unterminated group");
737
+ }
738
+ // Resolve numeric backreferences
739
+ for (const { node, branch, index } of backrefs.reverse()) {
740
+ if (groupCount < node.index) {
741
+ const digits = String(node.index);
742
+ if (!/^[0-7]+$/.test(digits)) {
743
+ let i = 0;
744
+ while (i < digits.length && OCTAL_DIGIT.test(digits[i]))
745
+ i++;
746
+ const headLen = i;
747
+ const replacement = [];
748
+ if (headLen > 0) {
749
+ replacement.push({
750
+ kind: "char",
751
+ value: parseInt(digits.slice(0, headLen), 8),
752
+ });
753
+ }
754
+ for (let j = headLen; j < digits.length; j++) {
755
+ replacement.push({ kind: "char", value: digits.charCodeAt(j) });
756
+ }
757
+ branch.splice(index, 1, ...replacement);
758
+ }
759
+ else {
760
+ branch[index] = { kind: "char", value: parseInt(digits, 8) };
761
+ }
762
+ }
763
+ }
764
+ // Resolve named backreferences
765
+ for (const { name, branch, index } of namedBackrefs) {
766
+ const groupIdx = nameToIndex.get(name);
767
+ if (groupIdx !== undefined) {
768
+ branch[index] = { kind: "backreference", index: groupIdx };
769
+ }
770
+ else {
771
+ throw new ParseError(pattern, "Invalid group name in \\k<...>");
772
+ }
773
+ }
774
+ return root;
775
+ }
776
+ //# sourceMappingURL=parser.js.map