trilean-regex 0.0.0 → 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.
@@ -0,0 +1,329 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("./errors.cjs");
3
+ const require_shorthand_classes = require("./shorthand-classes.cjs");
4
+ //#region src/parser.ts
5
+ /** Characters that end the current alternation branch: the branch separator itself, and the group closer, at whichever level `parseSequence` is currently reading. Encountering either at the top level, with no matching opener, is the caller's job to reject -- `parseSequence` simply stops, leaving it for the enclosing `parseAlternation`/`parseGroup`/top-level `parseRegex` to notice. */
6
+ const SEQUENCE_TERMINATORS = /* @__PURE__ */ new Set(["|", ")"]);
7
+ const SIMPLE_ESCAPES = {
8
+ ".": ".",
9
+ "*": "*",
10
+ "+": "+",
11
+ "?": "?",
12
+ "(": "(",
13
+ ")": ")",
14
+ "[": "[",
15
+ "]": "]",
16
+ "{": "{",
17
+ "}": "}",
18
+ "|": "|",
19
+ "^": "^",
20
+ $: "$",
21
+ "\\": "\\",
22
+ "/": "/",
23
+ n: "\n",
24
+ t: " ",
25
+ r: "\r",
26
+ f: "\f",
27
+ v: "\v"
28
+ };
29
+ /**
30
+ * Recursive-descent parser for the grammar documented in README.md. Each `parse*` method consumes exactly the construct it names and leaves `pos` positioned just past it; nothing here backtracks except the two speculative lookaheads (`tryParseBound`'s failure path, and `peekIsQuantifierStart`'s use of it), both of which restore `pos` themselves before returning.
31
+ *
32
+ * Holds no state beyond the source string and a cursor into it -- there is no token stream, no separate lexer pass. A character's meaning (metachar, literal, escape) is always decided in the context that is reading it (atom position, inside a class, right after a backslash), which is what a hand-written recursive-descent parser buys over a generic lexer/parser split for a grammar this size.
33
+ */
34
+ var Parser = class {
35
+ pattern;
36
+ pos = 0;
37
+ constructor(pattern) {
38
+ this.pattern = pattern;
39
+ }
40
+ get position() {
41
+ return this.pos;
42
+ }
43
+ atEnd() {
44
+ return this.pos >= this.pattern.length;
45
+ }
46
+ peek(offset = 0) {
47
+ return this.pattern[this.pos + offset];
48
+ }
49
+ advance() {
50
+ const char = this.pattern[this.pos];
51
+ if (char === void 0) throw new require_errors.RegexParseError(this.pattern, this.pos, "unexpected end of pattern");
52
+ this.pos += 1;
53
+ return char;
54
+ }
55
+ parseAlternation() {
56
+ const firstBranch = this.parseSequence();
57
+ const laterBranches = [];
58
+ while (this.peek() === "|") {
59
+ this.advance();
60
+ laterBranches.push(this.parseSequence());
61
+ }
62
+ return laterBranches.length === 0 ? firstBranch : {
63
+ kind: "alternation",
64
+ branches: [firstBranch, ...laterBranches]
65
+ };
66
+ }
67
+ parseSequence() {
68
+ if (this.atEnd() || SEQUENCE_TERMINATORS.has(this.peek() ?? "")) return {
69
+ kind: "concat",
70
+ operands: []
71
+ };
72
+ const firstOperand = this.parseQuantified();
73
+ const laterOperands = [];
74
+ while (!this.atEnd() && !SEQUENCE_TERMINATORS.has(this.peek() ?? "")) laterOperands.push(this.parseQuantified());
75
+ return laterOperands.length === 0 ? firstOperand : {
76
+ kind: "concat",
77
+ operands: [firstOperand, ...laterOperands]
78
+ };
79
+ }
80
+ parseQuantified() {
81
+ const atomStart = this.pos;
82
+ const atom = this.parseAtom();
83
+ if (atom.kind === "anchorStart" || atom.kind === "anchorEnd") {
84
+ if (this.peekIsQuantifierStart()) throw new require_errors.RegexParseError(this.pattern, atomStart, `an anchor ('${atom.kind === "anchorStart" ? "^" : "$"}') cannot be quantified`);
85
+ return atom;
86
+ }
87
+ const quantifier = this.tryParseQuantifier();
88
+ if (quantifier === void 0) return atom;
89
+ if (this.peekIsQuantifierStart()) throw new require_errors.RegexParseError(this.pattern, this.pos, "a quantifier cannot itself be quantified -- this grammar has no lazy or possessive modifiers, since a pattern that only ever answers a yes/no match question has no notion for either to change");
90
+ switch (quantifier.kind) {
91
+ case "star": return {
92
+ kind: "star",
93
+ operand: atom
94
+ };
95
+ case "plus": return {
96
+ kind: "plus",
97
+ operand: atom
98
+ };
99
+ case "optional": return {
100
+ kind: "optional",
101
+ operand: atom
102
+ };
103
+ case "repeat": return {
104
+ kind: "repeat",
105
+ operand: atom,
106
+ min: quantifier.min,
107
+ max: quantifier.max
108
+ };
109
+ default: throw new Error("unreachable quantifier kind");
110
+ }
111
+ }
112
+ /** A pure lookahead: restores `pos` before returning in every case, including through `tryParseBound`'s own successful parse. Used only to decide whether a second quantifier immediately follows the one just applied, which is always a parse error (see `parseQuantified`). */
113
+ peekIsQuantifierStart() {
114
+ const c = this.peek();
115
+ if (c === "*" || c === "+" || c === "?") return true;
116
+ if (c === "{") {
117
+ const savedPos = this.pos;
118
+ const bound = this.tryParseBound();
119
+ this.pos = savedPos;
120
+ return bound !== void 0;
121
+ }
122
+ return false;
123
+ }
124
+ tryParseQuantifier() {
125
+ const c = this.peek();
126
+ if (c === "*") {
127
+ this.advance();
128
+ return { kind: "star" };
129
+ }
130
+ if (c === "+") {
131
+ this.advance();
132
+ return { kind: "plus" };
133
+ }
134
+ if (c === "?") {
135
+ this.advance();
136
+ return { kind: "optional" };
137
+ }
138
+ if (c === "{") {
139
+ const bound = this.tryParseBound();
140
+ if (bound === void 0) return void 0;
141
+ return {
142
+ kind: "repeat",
143
+ min: bound.min,
144
+ max: bound.max
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Attempts `{min}` / `{min,}` / `{min,max}` starting at the current `{`. Rewinds and returns `undefined` for anything that is not exactly one of those three shapes -- including `{}`, `{,5}`, and a `{` with no closing brace at all -- so that an unrecognised `{` falls back to being parsed as a literal character by the caller, matching how a bounded-repetition quantifier is conventionally the only special meaning `{` ever carries.
150
+ *
151
+ * A backwards bound (`{5,2}`) is a real error rather than a fallback-to-literal case: the syntax is unambiguously a quantifier attempt, so this throws instead of returning `undefined` for it.
152
+ */
153
+ tryParseBound() {
154
+ const savedPos = this.pos;
155
+ this.pos += 1;
156
+ const minDigits = this.readDigits();
157
+ if (minDigits === "") {
158
+ this.pos = savedPos;
159
+ return;
160
+ }
161
+ let maxDigits;
162
+ let hasComma = false;
163
+ if (this.peek() === ",") {
164
+ hasComma = true;
165
+ this.pos += 1;
166
+ maxDigits = this.readDigits();
167
+ }
168
+ if (this.peek() !== "}") {
169
+ this.pos = savedPos;
170
+ return;
171
+ }
172
+ this.pos += 1;
173
+ const min = Number.parseInt(minDigits, 10);
174
+ const max = !hasComma ? min : maxDigits === "" ? void 0 : Number.parseInt(maxDigits ?? "", 10);
175
+ if (max !== void 0 && max < min) throw new require_errors.RegexParseError(this.pattern, savedPos, `quantifier range is backwards: {${minDigits}${hasComma ? "," + (maxDigits ?? "") : ""}} would require between ${String(min)} and ${String(max)} repetitions`);
176
+ return {
177
+ min,
178
+ max
179
+ };
180
+ }
181
+ readDigits() {
182
+ let digits = "";
183
+ for (;;) {
184
+ const c = this.peek();
185
+ if (c === void 0 || c < "0" || c > "9") break;
186
+ digits += c;
187
+ this.pos += 1;
188
+ }
189
+ return digits;
190
+ }
191
+ parseAtom() {
192
+ const c = this.peek();
193
+ if (c === void 0) throw new require_errors.RegexParseError(this.pattern, this.pos, "unexpected end of pattern");
194
+ if (c === "^") {
195
+ this.advance();
196
+ return { kind: "anchorStart" };
197
+ }
198
+ if (c === "$") {
199
+ this.advance();
200
+ return { kind: "anchorEnd" };
201
+ }
202
+ if (c === ".") {
203
+ this.advance();
204
+ return { kind: "anyChar" };
205
+ }
206
+ if (c === "(") return this.parseGroup();
207
+ if (c === "[") return this.parseCharClass();
208
+ if (c === "\\") return this.parseEscapedAtom();
209
+ if (c === "*" || c === "+" || c === "?") throw new require_errors.RegexParseError(this.pattern, this.pos, `quantifier '${c}' with nothing to repeat`);
210
+ this.advance();
211
+ return {
212
+ kind: "literal",
213
+ codeUnit: c.charCodeAt(0)
214
+ };
215
+ }
216
+ parseGroup() {
217
+ const start = this.pos;
218
+ this.advance();
219
+ if (this.peek() === "?" && this.peek(1) === ":") {
220
+ this.advance();
221
+ this.advance();
222
+ } else throw new require_errors.RegexParseError(this.pattern, start, "capturing groups are not supported in this grammar -- there is nothing to capture, since a pattern only ever answers whether it matches. Use '(?:...)' for grouping.");
223
+ const inner = this.parseAlternation();
224
+ if (this.peek() !== ")") throw new require_errors.RegexParseError(this.pattern, this.pos, "expected ')' to close '(?:'");
225
+ this.advance();
226
+ return inner;
227
+ }
228
+ parseEscapedAtom() {
229
+ const backslashPos = this.pos;
230
+ this.advance();
231
+ if (this.atEnd()) throw new require_errors.RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
232
+ const c = this.advance();
233
+ if (c === "d" || c === "D" || c === "w" || c === "W" || c === "s" || c === "S") return require_shorthand_classes.expandShorthandClass(c);
234
+ const literal = SIMPLE_ESCAPES[c];
235
+ if (literal !== void 0) return {
236
+ kind: "literal",
237
+ codeUnit: literal.charCodeAt(0)
238
+ };
239
+ throw new require_errors.RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${c}'`);
240
+ }
241
+ parseCharClass() {
242
+ const start = this.pos;
243
+ this.advance();
244
+ let negated = false;
245
+ if (this.peek() === "^") {
246
+ this.advance();
247
+ negated = true;
248
+ }
249
+ const ranges = [];
250
+ for (;;) {
251
+ if (this.atEnd()) throw new require_errors.RegexParseError(this.pattern, start, "unterminated character class -- missing closing ']'");
252
+ if (this.peek() === "]") {
253
+ if (ranges.length === 0) throw new require_errors.RegexParseError(this.pattern, this.pos, "a character class must contain at least one item -- ']' immediately, or immediately after '[^', is not the empty class here (escape it as '\\]' to match a literal ']')");
254
+ this.advance();
255
+ break;
256
+ }
257
+ ranges.push(...this.parseClassItem());
258
+ }
259
+ return {
260
+ kind: "charClass",
261
+ negated,
262
+ ranges
263
+ };
264
+ }
265
+ parseClassItem() {
266
+ const c = this.peek();
267
+ if (c === "\\") {
268
+ const backslashPos = this.pos;
269
+ this.advance();
270
+ if (this.atEnd()) throw new require_errors.RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
271
+ const escaped = this.advance();
272
+ if (escaped === "d" || escaped === "w" || escaped === "s") return require_shorthand_classes.expandShorthandClass(escaped).ranges;
273
+ if (escaped === "D" || escaped === "W" || escaped === "S") throw new require_errors.RegexParseError(this.pattern, backslashPos, `the negated shorthand class '\\${escaped}' cannot be embedded inside a character class, because its complement cannot be represented as a set of ranges to union with the class's other members -- use it as its own atom instead of embedding it here`);
274
+ const literal = SIMPLE_ESCAPES[escaped];
275
+ if (literal === void 0) throw new require_errors.RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${escaped}'`);
276
+ return this.parseOptionalRangeFrom(literal.charCodeAt(0));
277
+ }
278
+ if (c === void 0) throw new require_errors.RegexParseError(this.pattern, this.pos, "unterminated character class -- missing closing ']'");
279
+ this.advance();
280
+ return this.parseOptionalRangeFrom(c.charCodeAt(0));
281
+ }
282
+ parseOptionalRangeFrom(fromCode) {
283
+ if (this.peek() === "-" && this.peek(1) !== void 0 && this.peek(1) !== "]") {
284
+ const hyphenPos = this.pos;
285
+ this.advance();
286
+ const toCode = this.parseClassRangeEndpoint();
287
+ if (toCode < fromCode) throw new require_errors.RegexParseError(this.pattern, hyphenPos, `invalid range in character class: '${String.fromCharCode(fromCode)}-${String.fromCharCode(toCode)}' is reversed`);
288
+ return [{
289
+ from: fromCode,
290
+ to: toCode
291
+ }];
292
+ }
293
+ return [{
294
+ from: fromCode,
295
+ to: fromCode
296
+ }];
297
+ }
298
+ parseClassRangeEndpoint() {
299
+ const c = this.peek();
300
+ if (c === "\\") {
301
+ const backslashPos = this.pos;
302
+ this.advance();
303
+ if (this.atEnd()) throw new require_errors.RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
304
+ const escaped = this.advance();
305
+ if (escaped === "d" || escaped === "D" || escaped === "w" || escaped === "W" || escaped === "s" || escaped === "S") throw new require_errors.RegexParseError(this.pattern, backslashPos, `a shorthand class ('\\${escaped}') cannot be a range endpoint`);
306
+ const literal = SIMPLE_ESCAPES[escaped];
307
+ if (literal === void 0) throw new require_errors.RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${escaped}'`);
308
+ return literal.charCodeAt(0);
309
+ }
310
+ if (c === void 0) throw new require_errors.RegexParseError(this.pattern, this.pos, "unterminated character class -- missing closing ']'");
311
+ this.advance();
312
+ return c.charCodeAt(0);
313
+ }
314
+ };
315
+ /**
316
+ * Parses a pattern in this package's grammar (see README.md) into a `RegexNode` AST.
317
+ *
318
+ * Never returns a partial result: a pattern either parses completely, with nothing left over, or this throws `RegexParseError` naming exactly where parsing could not proceed.
319
+ *
320
+ * @throws {RegexParseError} if `pattern` is not a complete, valid pattern in this grammar.
321
+ */
322
+ function parseRegex(pattern) {
323
+ const parser = new Parser(pattern);
324
+ const node = parser.parseAlternation();
325
+ if (!parser.atEnd()) throw new require_errors.RegexParseError(pattern, parser.position, "unexpected character -- if this was meant literally, escape it with a backslash");
326
+ return node;
327
+ }
328
+ //#endregion
329
+ exports.parseRegex = parseRegex;
@@ -0,0 +1,12 @@
1
+ import { d as RegexNode } from "./ast-D8HLFxxv.cjs";
2
+ //#region src/parser.d.ts
3
+ /**
4
+ * Parses a pattern in this package's grammar (see README.md) into a `RegexNode` AST.
5
+ *
6
+ * Never returns a partial result: a pattern either parses completely, with nothing left over, or this throws `RegexParseError` naming exactly where parsing could not proceed.
7
+ *
8
+ * @throws {RegexParseError} if `pattern` is not a complete, valid pattern in this grammar.
9
+ */
10
+ declare function parseRegex(pattern: string): RegexNode;
11
+ //#endregion
12
+ export { parseRegex };
@@ -0,0 +1,12 @@
1
+ import { d as RegexNode } from "./ast-D8HLFxxv.js";
2
+ //#region src/parser.d.ts
3
+ /**
4
+ * Parses a pattern in this package's grammar (see README.md) into a `RegexNode` AST.
5
+ *
6
+ * Never returns a partial result: a pattern either parses completely, with nothing left over, or this throws `RegexParseError` naming exactly where parsing could not proceed.
7
+ *
8
+ * @throws {RegexParseError} if `pattern` is not a complete, valid pattern in this grammar.
9
+ */
10
+ declare function parseRegex(pattern: string): RegexNode;
11
+ //#endregion
12
+ export { parseRegex };
package/dist/parser.js ADDED
@@ -0,0 +1,328 @@
1
+ import { RegexParseError } from "./errors.js";
2
+ import { expandShorthandClass } from "./shorthand-classes.js";
3
+ //#region src/parser.ts
4
+ /** Characters that end the current alternation branch: the branch separator itself, and the group closer, at whichever level `parseSequence` is currently reading. Encountering either at the top level, with no matching opener, is the caller's job to reject -- `parseSequence` simply stops, leaving it for the enclosing `parseAlternation`/`parseGroup`/top-level `parseRegex` to notice. */
5
+ const SEQUENCE_TERMINATORS = /* @__PURE__ */ new Set(["|", ")"]);
6
+ const SIMPLE_ESCAPES = {
7
+ ".": ".",
8
+ "*": "*",
9
+ "+": "+",
10
+ "?": "?",
11
+ "(": "(",
12
+ ")": ")",
13
+ "[": "[",
14
+ "]": "]",
15
+ "{": "{",
16
+ "}": "}",
17
+ "|": "|",
18
+ "^": "^",
19
+ $: "$",
20
+ "\\": "\\",
21
+ "/": "/",
22
+ n: "\n",
23
+ t: " ",
24
+ r: "\r",
25
+ f: "\f",
26
+ v: "\v"
27
+ };
28
+ /**
29
+ * Recursive-descent parser for the grammar documented in README.md. Each `parse*` method consumes exactly the construct it names and leaves `pos` positioned just past it; nothing here backtracks except the two speculative lookaheads (`tryParseBound`'s failure path, and `peekIsQuantifierStart`'s use of it), both of which restore `pos` themselves before returning.
30
+ *
31
+ * Holds no state beyond the source string and a cursor into it -- there is no token stream, no separate lexer pass. A character's meaning (metachar, literal, escape) is always decided in the context that is reading it (atom position, inside a class, right after a backslash), which is what a hand-written recursive-descent parser buys over a generic lexer/parser split for a grammar this size.
32
+ */
33
+ var Parser = class {
34
+ pattern;
35
+ pos = 0;
36
+ constructor(pattern) {
37
+ this.pattern = pattern;
38
+ }
39
+ get position() {
40
+ return this.pos;
41
+ }
42
+ atEnd() {
43
+ return this.pos >= this.pattern.length;
44
+ }
45
+ peek(offset = 0) {
46
+ return this.pattern[this.pos + offset];
47
+ }
48
+ advance() {
49
+ const char = this.pattern[this.pos];
50
+ if (char === void 0) throw new RegexParseError(this.pattern, this.pos, "unexpected end of pattern");
51
+ this.pos += 1;
52
+ return char;
53
+ }
54
+ parseAlternation() {
55
+ const firstBranch = this.parseSequence();
56
+ const laterBranches = [];
57
+ while (this.peek() === "|") {
58
+ this.advance();
59
+ laterBranches.push(this.parseSequence());
60
+ }
61
+ return laterBranches.length === 0 ? firstBranch : {
62
+ kind: "alternation",
63
+ branches: [firstBranch, ...laterBranches]
64
+ };
65
+ }
66
+ parseSequence() {
67
+ if (this.atEnd() || SEQUENCE_TERMINATORS.has(this.peek() ?? "")) return {
68
+ kind: "concat",
69
+ operands: []
70
+ };
71
+ const firstOperand = this.parseQuantified();
72
+ const laterOperands = [];
73
+ while (!this.atEnd() && !SEQUENCE_TERMINATORS.has(this.peek() ?? "")) laterOperands.push(this.parseQuantified());
74
+ return laterOperands.length === 0 ? firstOperand : {
75
+ kind: "concat",
76
+ operands: [firstOperand, ...laterOperands]
77
+ };
78
+ }
79
+ parseQuantified() {
80
+ const atomStart = this.pos;
81
+ const atom = this.parseAtom();
82
+ if (atom.kind === "anchorStart" || atom.kind === "anchorEnd") {
83
+ if (this.peekIsQuantifierStart()) throw new RegexParseError(this.pattern, atomStart, `an anchor ('${atom.kind === "anchorStart" ? "^" : "$"}') cannot be quantified`);
84
+ return atom;
85
+ }
86
+ const quantifier = this.tryParseQuantifier();
87
+ if (quantifier === void 0) return atom;
88
+ if (this.peekIsQuantifierStart()) throw new RegexParseError(this.pattern, this.pos, "a quantifier cannot itself be quantified -- this grammar has no lazy or possessive modifiers, since a pattern that only ever answers a yes/no match question has no notion for either to change");
89
+ switch (quantifier.kind) {
90
+ case "star": return {
91
+ kind: "star",
92
+ operand: atom
93
+ };
94
+ case "plus": return {
95
+ kind: "plus",
96
+ operand: atom
97
+ };
98
+ case "optional": return {
99
+ kind: "optional",
100
+ operand: atom
101
+ };
102
+ case "repeat": return {
103
+ kind: "repeat",
104
+ operand: atom,
105
+ min: quantifier.min,
106
+ max: quantifier.max
107
+ };
108
+ default: throw new Error("unreachable quantifier kind");
109
+ }
110
+ }
111
+ /** A pure lookahead: restores `pos` before returning in every case, including through `tryParseBound`'s own successful parse. Used only to decide whether a second quantifier immediately follows the one just applied, which is always a parse error (see `parseQuantified`). */
112
+ peekIsQuantifierStart() {
113
+ const c = this.peek();
114
+ if (c === "*" || c === "+" || c === "?") return true;
115
+ if (c === "{") {
116
+ const savedPos = this.pos;
117
+ const bound = this.tryParseBound();
118
+ this.pos = savedPos;
119
+ return bound !== void 0;
120
+ }
121
+ return false;
122
+ }
123
+ tryParseQuantifier() {
124
+ const c = this.peek();
125
+ if (c === "*") {
126
+ this.advance();
127
+ return { kind: "star" };
128
+ }
129
+ if (c === "+") {
130
+ this.advance();
131
+ return { kind: "plus" };
132
+ }
133
+ if (c === "?") {
134
+ this.advance();
135
+ return { kind: "optional" };
136
+ }
137
+ if (c === "{") {
138
+ const bound = this.tryParseBound();
139
+ if (bound === void 0) return void 0;
140
+ return {
141
+ kind: "repeat",
142
+ min: bound.min,
143
+ max: bound.max
144
+ };
145
+ }
146
+ }
147
+ /**
148
+ * Attempts `{min}` / `{min,}` / `{min,max}` starting at the current `{`. Rewinds and returns `undefined` for anything that is not exactly one of those three shapes -- including `{}`, `{,5}`, and a `{` with no closing brace at all -- so that an unrecognised `{` falls back to being parsed as a literal character by the caller, matching how a bounded-repetition quantifier is conventionally the only special meaning `{` ever carries.
149
+ *
150
+ * A backwards bound (`{5,2}`) is a real error rather than a fallback-to-literal case: the syntax is unambiguously a quantifier attempt, so this throws instead of returning `undefined` for it.
151
+ */
152
+ tryParseBound() {
153
+ const savedPos = this.pos;
154
+ this.pos += 1;
155
+ const minDigits = this.readDigits();
156
+ if (minDigits === "") {
157
+ this.pos = savedPos;
158
+ return;
159
+ }
160
+ let maxDigits;
161
+ let hasComma = false;
162
+ if (this.peek() === ",") {
163
+ hasComma = true;
164
+ this.pos += 1;
165
+ maxDigits = this.readDigits();
166
+ }
167
+ if (this.peek() !== "}") {
168
+ this.pos = savedPos;
169
+ return;
170
+ }
171
+ this.pos += 1;
172
+ const min = Number.parseInt(minDigits, 10);
173
+ const max = !hasComma ? min : maxDigits === "" ? void 0 : Number.parseInt(maxDigits ?? "", 10);
174
+ if (max !== void 0 && max < min) throw new RegexParseError(this.pattern, savedPos, `quantifier range is backwards: {${minDigits}${hasComma ? "," + (maxDigits ?? "") : ""}} would require between ${String(min)} and ${String(max)} repetitions`);
175
+ return {
176
+ min,
177
+ max
178
+ };
179
+ }
180
+ readDigits() {
181
+ let digits = "";
182
+ for (;;) {
183
+ const c = this.peek();
184
+ if (c === void 0 || c < "0" || c > "9") break;
185
+ digits += c;
186
+ this.pos += 1;
187
+ }
188
+ return digits;
189
+ }
190
+ parseAtom() {
191
+ const c = this.peek();
192
+ if (c === void 0) throw new RegexParseError(this.pattern, this.pos, "unexpected end of pattern");
193
+ if (c === "^") {
194
+ this.advance();
195
+ return { kind: "anchorStart" };
196
+ }
197
+ if (c === "$") {
198
+ this.advance();
199
+ return { kind: "anchorEnd" };
200
+ }
201
+ if (c === ".") {
202
+ this.advance();
203
+ return { kind: "anyChar" };
204
+ }
205
+ if (c === "(") return this.parseGroup();
206
+ if (c === "[") return this.parseCharClass();
207
+ if (c === "\\") return this.parseEscapedAtom();
208
+ if (c === "*" || c === "+" || c === "?") throw new RegexParseError(this.pattern, this.pos, `quantifier '${c}' with nothing to repeat`);
209
+ this.advance();
210
+ return {
211
+ kind: "literal",
212
+ codeUnit: c.charCodeAt(0)
213
+ };
214
+ }
215
+ parseGroup() {
216
+ const start = this.pos;
217
+ this.advance();
218
+ if (this.peek() === "?" && this.peek(1) === ":") {
219
+ this.advance();
220
+ this.advance();
221
+ } else throw new RegexParseError(this.pattern, start, "capturing groups are not supported in this grammar -- there is nothing to capture, since a pattern only ever answers whether it matches. Use '(?:...)' for grouping.");
222
+ const inner = this.parseAlternation();
223
+ if (this.peek() !== ")") throw new RegexParseError(this.pattern, this.pos, "expected ')' to close '(?:'");
224
+ this.advance();
225
+ return inner;
226
+ }
227
+ parseEscapedAtom() {
228
+ const backslashPos = this.pos;
229
+ this.advance();
230
+ if (this.atEnd()) throw new RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
231
+ const c = this.advance();
232
+ if (c === "d" || c === "D" || c === "w" || c === "W" || c === "s" || c === "S") return expandShorthandClass(c);
233
+ const literal = SIMPLE_ESCAPES[c];
234
+ if (literal !== void 0) return {
235
+ kind: "literal",
236
+ codeUnit: literal.charCodeAt(0)
237
+ };
238
+ throw new RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${c}'`);
239
+ }
240
+ parseCharClass() {
241
+ const start = this.pos;
242
+ this.advance();
243
+ let negated = false;
244
+ if (this.peek() === "^") {
245
+ this.advance();
246
+ negated = true;
247
+ }
248
+ const ranges = [];
249
+ for (;;) {
250
+ if (this.atEnd()) throw new RegexParseError(this.pattern, start, "unterminated character class -- missing closing ']'");
251
+ if (this.peek() === "]") {
252
+ if (ranges.length === 0) throw new RegexParseError(this.pattern, this.pos, "a character class must contain at least one item -- ']' immediately, or immediately after '[^', is not the empty class here (escape it as '\\]' to match a literal ']')");
253
+ this.advance();
254
+ break;
255
+ }
256
+ ranges.push(...this.parseClassItem());
257
+ }
258
+ return {
259
+ kind: "charClass",
260
+ negated,
261
+ ranges
262
+ };
263
+ }
264
+ parseClassItem() {
265
+ const c = this.peek();
266
+ if (c === "\\") {
267
+ const backslashPos = this.pos;
268
+ this.advance();
269
+ if (this.atEnd()) throw new RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
270
+ const escaped = this.advance();
271
+ if (escaped === "d" || escaped === "w" || escaped === "s") return expandShorthandClass(escaped).ranges;
272
+ if (escaped === "D" || escaped === "W" || escaped === "S") throw new RegexParseError(this.pattern, backslashPos, `the negated shorthand class '\\${escaped}' cannot be embedded inside a character class, because its complement cannot be represented as a set of ranges to union with the class's other members -- use it as its own atom instead of embedding it here`);
273
+ const literal = SIMPLE_ESCAPES[escaped];
274
+ if (literal === void 0) throw new RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${escaped}'`);
275
+ return this.parseOptionalRangeFrom(literal.charCodeAt(0));
276
+ }
277
+ if (c === void 0) throw new RegexParseError(this.pattern, this.pos, "unterminated character class -- missing closing ']'");
278
+ this.advance();
279
+ return this.parseOptionalRangeFrom(c.charCodeAt(0));
280
+ }
281
+ parseOptionalRangeFrom(fromCode) {
282
+ if (this.peek() === "-" && this.peek(1) !== void 0 && this.peek(1) !== "]") {
283
+ const hyphenPos = this.pos;
284
+ this.advance();
285
+ const toCode = this.parseClassRangeEndpoint();
286
+ if (toCode < fromCode) throw new RegexParseError(this.pattern, hyphenPos, `invalid range in character class: '${String.fromCharCode(fromCode)}-${String.fromCharCode(toCode)}' is reversed`);
287
+ return [{
288
+ from: fromCode,
289
+ to: toCode
290
+ }];
291
+ }
292
+ return [{
293
+ from: fromCode,
294
+ to: fromCode
295
+ }];
296
+ }
297
+ parseClassRangeEndpoint() {
298
+ const c = this.peek();
299
+ if (c === "\\") {
300
+ const backslashPos = this.pos;
301
+ this.advance();
302
+ if (this.atEnd()) throw new RegexParseError(this.pattern, backslashPos, "trailing backslash with nothing to escape");
303
+ const escaped = this.advance();
304
+ if (escaped === "d" || escaped === "D" || escaped === "w" || escaped === "W" || escaped === "s" || escaped === "S") throw new RegexParseError(this.pattern, backslashPos, `a shorthand class ('\\${escaped}') cannot be a range endpoint`);
305
+ const literal = SIMPLE_ESCAPES[escaped];
306
+ if (literal === void 0) throw new RegexParseError(this.pattern, backslashPos, `unknown escape sequence '\\${escaped}'`);
307
+ return literal.charCodeAt(0);
308
+ }
309
+ if (c === void 0) throw new RegexParseError(this.pattern, this.pos, "unterminated character class -- missing closing ']'");
310
+ this.advance();
311
+ return c.charCodeAt(0);
312
+ }
313
+ };
314
+ /**
315
+ * Parses a pattern in this package's grammar (see README.md) into a `RegexNode` AST.
316
+ *
317
+ * Never returns a partial result: a pattern either parses completely, with nothing left over, or this throws `RegexParseError` naming exactly where parsing could not proceed.
318
+ *
319
+ * @throws {RegexParseError} if `pattern` is not a complete, valid pattern in this grammar.
320
+ */
321
+ function parseRegex(pattern) {
322
+ const parser = new Parser(pattern);
323
+ const node = parser.parseAlternation();
324
+ if (!parser.atEnd()) throw new RegexParseError(pattern, parser.position, "unexpected character -- if this was meant literally, escape it with a backslash");
325
+ return node;
326
+ }
327
+ //#endregion
328
+ export { parseRegex };