regex-explain 0.0.0-stage → 1.0.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.
@@ -0,0 +1,628 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRegexSource = isRegexSource;
4
+ exports.explainRegex = explainRegex;
5
+ exports.parseRegexInput = parseRegexInput;
6
+ exports.describeCount = describeCount;
7
+ /**
8
+ * Turns an ECMAScript regular expression into a tree of plain-English explanations.
9
+ *
10
+ * Parsing is done by `@eslint-community/regexpp`, character set and length analysis by
11
+ * `regexp-ast-analysis`, and backtracking analysis by `scslre`. This module walks the AST and words
12
+ * the results.
13
+ */
14
+ const regexpp_1 = require("@eslint-community/regexpp");
15
+ const regexp_ast_analysis_1 = require("regexp-ast-analysis");
16
+ const scslre_1 = require("scslre");
17
+ const text_1 = require("./text");
18
+ /**
19
+ * Checks whether a value is a {@link RegexSource}, such as an untyped command argument.
20
+ *
21
+ * @param value The value.
22
+ *
23
+ * @returns Whether the value has string `pattern` and `flags` properties.
24
+ */
25
+ function isRegexSource(value) {
26
+ const candidate = value;
27
+ return typeof candidate?.pattern === "string" && typeof candidate.flags === "string";
28
+ }
29
+ /** Flag letters mapped to their `RegExp` property and meaning. */
30
+ const FLAG_INFO = {
31
+ d: { name: "hasIndices", description: "Record start and end indices for each capture group." },
32
+ g: { name: "global", description: "Find all matches instead of stopping after the first." },
33
+ i: { name: "ignoreCase", description: "Ignore case: letters match both upper and lower case." },
34
+ m: {
35
+ name: "multiline",
36
+ description: `${(0, text_1.code)("^")} and ${(0, text_1.code)("$")} match at the start and end of each line, not just the whole input.`,
37
+ },
38
+ s: { name: "dotAll", description: `${(0, text_1.code)(".")} also matches line terminators.` },
39
+ u: {
40
+ name: "unicode",
41
+ description: "Treat the pattern as a sequence of Unicode code points and use strict syntax.",
42
+ },
43
+ v: {
44
+ name: "unicodeSets",
45
+ description: `Unicode mode with set operations (${(0, text_1.code)("--")}, ${(0, text_1.code)("&&")}), nested classes, and string properties.`,
46
+ },
47
+ y: { name: "sticky", description: `Match only at the position given by ${(0, text_1.code)("lastIndex")}.` },
48
+ };
49
+ /** Friendly names for common Unicode properties and general categories. */
50
+ const UNICODE_PROPERTY_NAMES = {
51
+ L: "letter",
52
+ Letter: "letter",
53
+ Lu: "uppercase letter",
54
+ Uppercase_Letter: "uppercase letter",
55
+ Ll: "lowercase letter",
56
+ Lowercase_Letter: "lowercase letter",
57
+ Lt: "titlecase letter",
58
+ Lm: "modifier letter",
59
+ Lo: "other letter",
60
+ M: "mark",
61
+ Mark: "mark",
62
+ N: "number",
63
+ Number: "number",
64
+ Nd: "decimal digit",
65
+ Decimal_Number: "decimal digit",
66
+ P: "punctuation character",
67
+ Punctuation: "punctuation character",
68
+ S: "symbol",
69
+ Symbol: "symbol",
70
+ Sc: "currency symbol",
71
+ Currency_Symbol: "currency symbol",
72
+ Z: "separator",
73
+ Separator: "separator",
74
+ Zs: "space separator",
75
+ Space_Separator: "space separator",
76
+ C: "control or other character",
77
+ Other: "control or other character",
78
+ Cc: "control character",
79
+ Control: "control character",
80
+ Alphabetic: "alphabetic character",
81
+ Alpha: "alphabetic character",
82
+ White_Space: "whitespace character",
83
+ space: "whitespace character",
84
+ Emoji: "emoji character",
85
+ ASCII: "ASCII character",
86
+ Any: "character",
87
+ };
88
+ /** Property keys whose value names a script. */
89
+ const SCRIPT_KEYS = new Set(["Script", "sc", "Script_Extensions", "scx"]);
90
+ /**
91
+ * Explains a regex pattern compiled with the given flags. Syntax errors are returned, not thrown.
92
+ *
93
+ * @param pattern The pattern without surrounding slashes, as passed to `new RegExp()`.
94
+ * @param flags The flags, such as `"gi"`.
95
+ * @param options Analysis options.
96
+ *
97
+ * @returns The explanation, or the reason the pattern is invalid.
98
+ */
99
+ function explainRegex(pattern, flags = "", options = {}) {
100
+ const parser = new regexpp_1.RegExpParser({ ecmaVersion: 2025 });
101
+ let flagsAst;
102
+ let patternAst;
103
+ try {
104
+ flagsAst = parser.parseFlags(flags);
105
+ patternAst = parser.parsePattern(pattern, 0, pattern.length, {
106
+ unicode: flagsAst.unicode,
107
+ unicodeSets: flagsAst.unicodeSets,
108
+ });
109
+ }
110
+ catch (error) {
111
+ return {
112
+ ok: false,
113
+ pattern,
114
+ flags,
115
+ message: error instanceof Error ? error.message : String(error),
116
+ index: error instanceof regexpp_1.RegExpSyntaxError ? error.index : undefined,
117
+ };
118
+ }
119
+ const ctx = { flags: flagsAst };
120
+ const length = (0, regexp_ast_analysis_1.getLengthRange)(patternAst.alternatives, flagsAst);
121
+ return {
122
+ ok: true,
123
+ pattern,
124
+ flags,
125
+ nodes: explainAlternatives(patternAst.alternatives, ctx),
126
+ flagExplanations: [...flags].map((flag) => ({ flag, ...FLAG_INFO[flag] })),
127
+ length: { min: length.min, max: length.max },
128
+ warnings: options.checkBacktracking === false ? [] : findBacktracking(patternAst, flagsAst, pattern),
129
+ multiline: flagsAst.multiline || enablesMultiline(patternAst),
130
+ };
131
+ }
132
+ /**
133
+ * Checks whether a node, or any node inside it, is a group whose modifiers turn multiline on, as in
134
+ * `(?m:^a$)`.
135
+ *
136
+ * @param node The node.
137
+ *
138
+ * @returns Whether `^` and `$` match at line breaks somewhere in the node.
139
+ */
140
+ function enablesMultiline(node) {
141
+ if (node.type === "Group" && node.modifiers?.add.multiline) {
142
+ return true;
143
+ }
144
+ const children = "alternatives" in node
145
+ ? node.alternatives
146
+ : node.type === "Alternative"
147
+ ? node.elements
148
+ : node.type === "Quantifier"
149
+ ? [node.element]
150
+ : [];
151
+ return children.some(enablesMultiline);
152
+ }
153
+ /**
154
+ * Reads typed or selected text as a regex.
155
+ *
156
+ * @param text Either a literal such as `/ab+c/gi`, optionally surrounded by whitespace, or a bare
157
+ * pattern.
158
+ *
159
+ * @returns The regex; bare patterns have no flags. The pattern is not validated.
160
+ */
161
+ function parseRegexInput(text) {
162
+ const literal = /^\/((?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\[])+)\/([a-z]*)$/s.exec(text.trim());
163
+ return literal ? { pattern: literal[1], flags: literal[2] } : { pattern: text, flags: "" };
164
+ }
165
+ /**
166
+ * Finds quantifiers that can backtrack catastrophically.
167
+ *
168
+ * @param pattern The parsed pattern.
169
+ * @param flags The parsed flags.
170
+ * @param source The pattern source, for slicing fixes.
171
+ *
172
+ * @returns One warning per problem found.
173
+ */
174
+ function findBacktracking(pattern, flags, source) {
175
+ // Move reports flag ordinary patterns such as /a+b/ that are slow only on adversarial input
176
+ // scanned at every offset; they are too common to be useful as warnings.
177
+ const { reports } = (0, scslre_1.analyse)({ pattern, flags }, { reportTypes: { Move: false } });
178
+ return reports.map((report) => toWarning(report, source));
179
+ }
180
+ /**
181
+ * Words one backtracking report.
182
+ *
183
+ * @param report The scslre report.
184
+ * @param source The pattern source.
185
+ *
186
+ * @returns The warning.
187
+ */
188
+ function toWarning(report, source) {
189
+ const quant = report.type === "Trade" ? report.startQuant : report.quant;
190
+ const cost = `so a failed match can take ${report.exponential ? "exponential" : "polynomial"} time`;
191
+ let description;
192
+ switch (report.type) {
193
+ case "Trade":
194
+ description = `${(0, text_1.code)(report.startQuant.raw)} and ${(0, text_1.code)(report.endQuant.raw)} can match the same text, ${cost}`;
195
+ break;
196
+ case "Self":
197
+ description = `${(0, text_1.code)(report.quant.raw)} is repeated by ${(0, text_1.code)(report.parentQuant.raw)} and can match the same text in many ways, ${cost}`;
198
+ break;
199
+ case "Move":
200
+ // Disabled in findBacktracking; worded anyway so new options cannot produce blank warnings.
201
+ description = `${(0, text_1.code)(report.quant.raw)} is retried at every position of a failing input, ${cost}`;
202
+ break;
203
+ }
204
+ const fixed = report.fix();
205
+ return {
206
+ raw: source.slice(quant.start, quant.end),
207
+ start: quant.start,
208
+ end: quant.end,
209
+ exponential: report.exponential,
210
+ attackCharacter: report.character.pick,
211
+ description,
212
+ fix: fixed ? { pattern: fixed.source, flags: fixed.flags } : undefined,
213
+ };
214
+ }
215
+ /**
216
+ * Builds an explanation node.
217
+ *
218
+ * @param span The source the node explains.
219
+ * @param description The explanation.
220
+ * @param children Explanations of the parts of `span`.
221
+ *
222
+ * @returns The node.
223
+ */
224
+ function make(span, description, children = []) {
225
+ return { raw: span.raw, start: span.start, end: span.end, description, children };
226
+ }
227
+ /**
228
+ * Returns a copy of a node with a prefix added to its description.
229
+ *
230
+ * @param prefix The text to add.
231
+ * @param node The node.
232
+ *
233
+ * @returns The new node.
234
+ */
235
+ function prefixed(prefix, node) {
236
+ return { ...node, description: prefix + node.description };
237
+ }
238
+ /**
239
+ * Explains a list of alternatives, as found in the pattern, groups, and lookarounds.
240
+ *
241
+ * @param alternatives The alternatives, at least one.
242
+ * @param ctx The walk context.
243
+ *
244
+ * @returns The steps of a single alternative, or one node that lists every alternative.
245
+ */
246
+ function explainAlternatives(alternatives, ctx) {
247
+ if (alternatives.length === 1) {
248
+ return explainSequence(alternatives[0], ctx);
249
+ }
250
+ const options = alternatives.map((alt, i) => {
251
+ const steps = explainSequence(alt, ctx);
252
+ // An option with a single step reads better without an extra level.
253
+ if (steps.length === 1) {
254
+ return make(alt, `Option ${i + 1}: ${(0, text_1.lowerFirst)(steps[0].description)}`, [
255
+ ...steps[0].children,
256
+ ]);
257
+ }
258
+ return make(alt, `Option ${i + 1}:`, steps);
259
+ });
260
+ const span = {
261
+ raw: alternatives.map((a) => a.raw).join("|"),
262
+ start: alternatives[0].start,
263
+ end: alternatives[alternatives.length - 1].end,
264
+ };
265
+ return [make(span, `Match one of ${alternatives.length} alternatives:`, options)];
266
+ }
267
+ /**
268
+ * Explains the elements of one alternative, merging runs of literal characters.
269
+ *
270
+ * @param alt The alternative.
271
+ * @param ctx The walk context.
272
+ *
273
+ * @returns One node per step.
274
+ */
275
+ function explainSequence(alt, ctx) {
276
+ if (alt.elements.length === 0) {
277
+ return [make(alt, "Match the empty string")];
278
+ }
279
+ // Consecutive characters become one run, so that "abc" is one step rather than three.
280
+ const steps = alt.elements.reduce((acc, el) => {
281
+ const last = acc[acc.length - 1];
282
+ if (el.type !== "Character") {
283
+ return [...acc, el];
284
+ }
285
+ return Array.isArray(last) ? [...acc.slice(0, -1), [...last, el]] : [...acc, [el]];
286
+ }, []);
287
+ return steps.map((step) => Array.isArray(step) ? explainCharacterRun(step, ctx) : explainElement(step, ctx));
288
+ }
289
+ /**
290
+ * Returns " (case-insensitive)" when ignoring case changes what the elements match.
291
+ *
292
+ * @param elements The elements to check.
293
+ * @param ctx The walk context.
294
+ *
295
+ * @returns The note, or an empty string.
296
+ */
297
+ function caseNote(elements, ctx) {
298
+ if (!ctx.flags.ignoreCase) {
299
+ return "";
300
+ }
301
+ const sensitive = { ...ctx.flags, ignoreCase: false };
302
+ const changes = elements.some((el) => (0, regexp_ast_analysis_1.toUnicodeSet)(el, ctx.flags).chars.size !== (0, regexp_ast_analysis_1.toUnicodeSet)(el, sensitive).chars.size);
303
+ return changes ? " (case-insensitive)" : "";
304
+ }
305
+ /**
306
+ * Explains consecutive literal characters as one step.
307
+ *
308
+ * @param run The characters, at least one.
309
+ * @param ctx The walk context.
310
+ *
311
+ * @returns The node.
312
+ */
313
+ function explainCharacterRun(run, ctx) {
314
+ const note = caseNote(run, ctx);
315
+ if (run.length === 1) {
316
+ return make(run[0], `Match ${(0, text_1.describeCodePoint)(run[0].value)}${note}`);
317
+ }
318
+ const text = String.fromCodePoint(...run.map((c) => c.value));
319
+ const span = {
320
+ raw: run.map((c) => c.raw).join(""),
321
+ start: run[0].start,
322
+ end: run[run.length - 1].end,
323
+ };
324
+ // Only call it text when every character is visible; otherwise name each one.
325
+ if (/^[^\p{C}\s](?:[^\p{C}]*[^\p{C}\s])?$/u.test(text)) {
326
+ return make(span, `Match the text ${(0, text_1.code)(text)}${note}`);
327
+ }
328
+ const names = run.map((c) => (0, text_1.describeCodePoint)(c.value)).join(", ");
329
+ return make(span, `Match the sequence ${names}${note}`);
330
+ }
331
+ /**
332
+ * Explains one element of an alternative.
333
+ *
334
+ * @param el The element.
335
+ * @param ctx The walk context.
336
+ *
337
+ * @returns The node.
338
+ */
339
+ function explainElement(el, ctx) {
340
+ switch (el.type) {
341
+ case "Assertion":
342
+ return explainAssertion(el, ctx);
343
+ case "Quantifier":
344
+ return explainQuantifier(el, ctx);
345
+ default:
346
+ return explainQuantifiable(el, ctx);
347
+ }
348
+ }
349
+ /**
350
+ * Explains an element that a quantifier can repeat.
351
+ *
352
+ * @param el The element.
353
+ * @param ctx The walk context.
354
+ *
355
+ * @returns The node.
356
+ */
357
+ function explainQuantifiable(el, ctx) {
358
+ switch (el.type) {
359
+ case "Character":
360
+ return explainCharacterRun([el], ctx);
361
+ case "CapturingGroup": {
362
+ const n = (0, regexp_ast_analysis_1.getCapturingGroupNumber)(el);
363
+ const label = el.name ? `Capture group #${n} named ${(0, text_1.code)(el.name)}` : `Capture group #${n}`;
364
+ return make(el, `${label}:`, explainAlternatives(el.alternatives, ctx));
365
+ }
366
+ case "Group": {
367
+ if (!el.modifiers) {
368
+ return make(el, "Group (non-capturing):", explainAlternatives(el.alternatives, ctx));
369
+ }
370
+ const inner = applyModifiers(el.modifiers, ctx);
371
+ const label = `Group with modifiers (${describeModifiers(el.modifiers)}):`;
372
+ return make(el, label, explainAlternatives(el.alternatives, inner));
373
+ }
374
+ case "Backreference":
375
+ return make(el, `Match the same text last captured by ${describeBackrefTarget(el)}`);
376
+ case "Assertion":
377
+ // Lookaheads are quantifiable in legacy (non-unicode) mode.
378
+ return explainAssertion(el, ctx);
379
+ case "CharacterClass":
380
+ case "ExpressionCharacterClass":
381
+ return explainClass(el, ctx, true);
382
+ case "CharacterSet":
383
+ return make(el, `Match ${describeCharacterSet(el, ctx)}${caseNote([el], ctx)}`);
384
+ }
385
+ }
386
+ /**
387
+ * Explains a quantifier by appending the repetition to its element's explanation.
388
+ *
389
+ * @param q The quantifier.
390
+ * @param ctx The walk context.
391
+ *
392
+ * @returns The node.
393
+ */
394
+ function explainQuantifier(q, ctx) {
395
+ const inner = explainQuantifiable(q.element, ctx);
396
+ const greed = q.min === q.max ? "" : q.greedy ? " (as many as possible)" : " (as few as possible)";
397
+ const repetition = `${describeCount(q.min, q.max)}${greed}`;
398
+ if (inner.children.length === 0) {
399
+ return make(q, `${inner.description}, ${repetition}`);
400
+ }
401
+ const heading = inner.description.replace(/:$/, "");
402
+ return make(q, `${heading}, ${repetition}:`, [...inner.children]);
403
+ }
404
+ /**
405
+ * Words a repetition count.
406
+ *
407
+ * @param min The minimum count.
408
+ * @param max The maximum count; `Infinity` when unbounded.
409
+ *
410
+ * @returns A phrase such as "one or more times".
411
+ */
412
+ function describeCount(min, max) {
413
+ if (min === 0 && max === 1) {
414
+ return "optionally";
415
+ }
416
+ if (max === Infinity) {
417
+ return min === 0
418
+ ? "zero or more times"
419
+ : min === 1
420
+ ? "one or more times"
421
+ : `${min} or more times`;
422
+ }
423
+ if (min === max) {
424
+ return min === 1 ? "exactly once" : `exactly ${min} times`;
425
+ }
426
+ return `between ${min} and ${max} times`;
427
+ }
428
+ /**
429
+ * Explains an anchor, word boundary, or lookaround.
430
+ *
431
+ * @param a The assertion.
432
+ * @param ctx The walk context.
433
+ *
434
+ * @returns The node.
435
+ */
436
+ function explainAssertion(a, ctx) {
437
+ const where = ctx.flags.multiline ? "a line" : "the input";
438
+ switch (a.kind) {
439
+ case "start":
440
+ return make(a, `Assert position is at the start of ${where}`);
441
+ case "end":
442
+ return make(a, `Assert position is at the end of ${where}`);
443
+ case "word":
444
+ return make(a, `Assert position is ${a.negate ? "not " : ""}at a word boundary`);
445
+ case "lookahead":
446
+ return make(a, a.negate
447
+ ? "Negative lookahead: assert that what follows does not match:"
448
+ : "Lookahead: assert that what follows matches:", explainAlternatives(a.alternatives, ctx));
449
+ case "lookbehind":
450
+ return make(a, a.negate
451
+ ? "Negative lookbehind: assert that what precedes does not match:"
452
+ : "Lookbehind: assert that what precedes matches:", explainAlternatives(a.alternatives, ctx));
453
+ }
454
+ }
455
+ /**
456
+ * Names the group a backreference refers to.
457
+ *
458
+ * @param ref The backreference.
459
+ *
460
+ * @returns A phrase such as "group #1 (`name`)".
461
+ */
462
+ function describeBackrefTarget(ref) {
463
+ if (ref.ambiguous) {
464
+ const numbers = ref.resolved.map((g) => `#${(0, regexp_ast_analysis_1.getCapturingGroupNumber)(g)}`).join(" or ");
465
+ return `the group named ${(0, text_1.code)(ref.ref)} (${numbers})`;
466
+ }
467
+ const n = (0, regexp_ast_analysis_1.getCapturingGroupNumber)(ref.resolved);
468
+ return typeof ref.ref === "string" ? `group #${n} (${(0, text_1.code)(ref.ref)})` : `group #${n}`;
469
+ }
470
+ /**
471
+ * Applies a group's inline modifiers, such as `(?i-m:...)`, to the context.
472
+ *
473
+ * @param m The modifiers.
474
+ * @param ctx The enclosing context.
475
+ *
476
+ * @returns The context inside the group.
477
+ */
478
+ function applyModifiers(m, ctx) {
479
+ // A flag a group removes is off, one it adds is on, and the rest keep the enclosing value.
480
+ const flag = (name) => m.remove?.[name] ? false : m.add?.[name] ? true : ctx.flags[name];
481
+ return {
482
+ flags: {
483
+ ...ctx.flags,
484
+ ignoreCase: flag("ignoreCase"),
485
+ multiline: flag("multiline"),
486
+ dotAll: flag("dotAll"),
487
+ },
488
+ };
489
+ }
490
+ /**
491
+ * Words a group's inline modifiers.
492
+ *
493
+ * @param m The modifiers.
494
+ *
495
+ * @returns A phrase such as "enable ignore case; disable multiline".
496
+ */
497
+ function describeModifiers(m) {
498
+ const names = (f) => f
499
+ ? [
500
+ f.ignoreCase ? "ignore case" : "",
501
+ f.multiline ? "multiline" : "",
502
+ f.dotAll ? "dotAll" : "",
503
+ ].filter(Boolean)
504
+ : [];
505
+ const on = names(m.add);
506
+ const off = names(m.remove);
507
+ return [on.length ? `enable ${on.join(", ")}` : "", off.length ? `disable ${off.join(", ")}` : ""]
508
+ .filter(Boolean)
509
+ .join("; ");
510
+ }
511
+ /**
512
+ * Explains a character class.
513
+ *
514
+ * @param cls The class.
515
+ * @param ctx The walk context.
516
+ * @param topLevel Whether the class is an element of an alternative, which adds the verb "Match".
517
+ *
518
+ * @returns The node.
519
+ */
520
+ function explainClass(cls, ctx, topLevel) {
521
+ const verb = topLevel ? "Match " : "";
522
+ const note = topLevel ? caseNote([cls], ctx) : "";
523
+ if (cls.type === "ExpressionCharacterClass") {
524
+ const what = cls.negate ? "any character not in" : "a character in";
525
+ return make(cls, `${verb}${what} the set${note}:`, [explainClassElement(cls.expression, ctx)]);
526
+ }
527
+ if (cls.elements.length === 0) {
528
+ return make(cls, cls.negate ? `${verb}any character at all` : `${verb}nothing (an empty class never matches)`);
529
+ }
530
+ const what = cls.negate ? "any character except" : "one of";
531
+ return make(cls, `${verb}${what}${note}:`, cls.elements.map((e) => explainClassElement(e, ctx)));
532
+ }
533
+ /**
534
+ * Explains one member of a character class as a noun phrase.
535
+ *
536
+ * @param el The member.
537
+ * @param ctx The walk context.
538
+ *
539
+ * @returns The node.
540
+ */
541
+ function explainClassElement(el, ctx) {
542
+ switch (el.type) {
543
+ case "Character":
544
+ return make(el, (0, text_1.describeCodePoint)(el.value));
545
+ case "CharacterClassRange":
546
+ return make(el, `a character from ${(0, text_1.describeCodePoint)(el.min.value)} to ${(0, text_1.describeCodePoint)(el.max.value)}`);
547
+ case "CharacterSet":
548
+ return make(el, describeCharacterSet(el, ctx));
549
+ case "CharacterClass":
550
+ case "ExpressionCharacterClass":
551
+ return explainClass(el, ctx, false);
552
+ case "ClassStringDisjunction": {
553
+ const strings = el.alternatives.map((a) => a.elements.length
554
+ ? (0, text_1.code)(String.fromCodePoint(...a.elements.map((c) => c.value)))
555
+ : "the empty string");
556
+ return make(el, strings.length === 1
557
+ ? `the string ${strings[0]}`
558
+ : `one of the strings ${strings.join(", ")}`);
559
+ }
560
+ case "ClassIntersection":
561
+ return make(el, "characters that are in all of:", flattenOperands(el).map((o) => explainClassElement(o, ctx)));
562
+ case "ClassSubtraction": {
563
+ const [first, ...rest] = flattenOperands(el);
564
+ return make(el, "characters that are:", [
565
+ prefixed("in ", explainClassElement(first, ctx)),
566
+ ...rest.map((o) => prefixed("but not ", explainClassElement(o, ctx))),
567
+ ]);
568
+ }
569
+ }
570
+ }
571
+ /**
572
+ * Flattens a left-nested set operation such as `(a&&b)&&c` into its operands.
573
+ *
574
+ * @param node The operation.
575
+ *
576
+ * @returns The operands, left to right.
577
+ */
578
+ function flattenOperands(node) {
579
+ const left = node.left.type === node.type
580
+ ? flattenOperands(node.left)
581
+ : [node.left];
582
+ return [...left, node.right];
583
+ }
584
+ /**
585
+ * Describes `.`, `\d`, `\s`, `\w`, or a Unicode property escape as a noun phrase.
586
+ *
587
+ * @param set The character set.
588
+ * @param ctx The walk context.
589
+ *
590
+ * @returns The phrase.
591
+ */
592
+ function describeCharacterSet(set, ctx) {
593
+ switch (set.kind) {
594
+ case "any":
595
+ return ctx.flags.dotAll ? "any character" : "any character except a line break";
596
+ case "digit":
597
+ return set.negate ? "any character that is not a digit" : "a digit (0-9)";
598
+ case "space":
599
+ return set.negate
600
+ ? "any character that is not whitespace"
601
+ : "a whitespace character (space, tab, line break, etc.)";
602
+ case "word":
603
+ return set.negate
604
+ ? "any character that is not a word character"
605
+ : "a word character (letter, digit, or underscore)";
606
+ case "property":
607
+ return describeProperty(set);
608
+ }
609
+ }
610
+ /**
611
+ * Describes a Unicode property escape such as `\p{Lu}` as a noun phrase.
612
+ *
613
+ * @param set The property escape.
614
+ *
615
+ * @returns The phrase.
616
+ */
617
+ function describeProperty(set) {
618
+ const raw = set.value ? `${set.key}=${set.value}` : set.key;
619
+ if (set.strings) {
620
+ return `a string with Unicode property ${(0, text_1.code)(raw)}`;
621
+ }
622
+ const name = set.value ?? set.key;
623
+ const friendly = set.value && SCRIPT_KEYS.has(set.key)
624
+ ? `character in the ${set.value.replace(/_/g, " ")} script`
625
+ : (UNICODE_PROPERTY_NAMES[name] ?? `character with Unicode property ${(0, text_1.code)(raw)}`);
626
+ return set.negate ? `any character that is not ${(0, text_1.withArticle)(friendly)}` : (0, text_1.withArticle)(friendly);
627
+ }
628
+ //# sourceMappingURL=explainer.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Links that open a regex in external tools, with their fields prefilled. Each tool has one builder
3
+ * function here.
4
+ *
5
+ * - [regex101](https://regex101.com) tests and debugs regexes interactively. Its fields are prefilled
6
+ * from the query string, as its FAQ describes:
7
+ * https://github.com/firasdib/Regex101/wiki/FAQ#how-to-prefill-the-fields-on-the-interface-via-url
8
+ * The FAQ does not list `flavor`, but regex101 reads it: `javascript` selects the JavaScript
9
+ * flavor.
10
+ */
11
+ import type { RegexSource } from "./explainer";
12
+ /**
13
+ * Builds a regex101 link that opens a regex in the JavaScript flavor, which is the engine this
14
+ * library explains and matches with.
15
+ *
16
+ * @param source The regex.
17
+ * @param testString Text to match against; left out when empty.
18
+ *
19
+ * @returns The link, with each value percent-encoded so that `+` and spaces survive.
20
+ */
21
+ export declare function regex101Url(source: RegexSource, testString?: string): string;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.regex101Url = regex101Url;
4
+ /** The regex101 page the fields are prefilled on. */
5
+ const REGEX101 = "https://regex101.com/";
6
+ /**
7
+ * Builds a regex101 link that opens a regex in the JavaScript flavor, which is the engine this
8
+ * library explains and matches with.
9
+ *
10
+ * @param source The regex.
11
+ * @param testString Text to match against; left out when empty.
12
+ *
13
+ * @returns The link, with each value percent-encoded so that `+` and spaces survive.
14
+ */
15
+ function regex101Url(source, testString = "") {
16
+ const params = [
17
+ ["regex", source.pattern],
18
+ ["flags", source.flags],
19
+ ["flavor", "javascript"],
20
+ ...(testString === "" ? [] : [["testString", testString]]),
21
+ ];
22
+ const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
23
+ return `${REGEX101}?${query}`;
24
+ }
25
+ //# sourceMappingURL=extlinks.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Explains ECMAScript regular expressions in plain English, finds regexes in source code, and runs
3
+ * them over text.
4
+ *
5
+ * ```ts
6
+ * import { ExplanationRenderer, explainRegex } from "regex-explain";
7
+ *
8
+ * const result = explainRegex("^\\d{3}-\\d{4}$");
9
+ * console.log(new ExplanationRenderer(result).markdownList());
10
+ * ```
11
+ */
12
+ export type { BacktrackingWarning, ExplainOptions, ExplainResult, ExplanationNode, FlagExplanation, LengthSummary, RegexExplanation, RegexExplanationError, RegexSource, } from "./explainer";
13
+ export { describeCount, explainRegex, isRegexSource, parseRegexInput } from "./explainer";
14
+ export { GRAMMARS, SUPPORTED_LANGUAGE_IDS } from "./compiler";
15
+ export type { LocatedRegex } from "./locator";
16
+ export { findRegexAt, RegexFinder } from "./locator";
17
+ export type { FoundMatch, MatchesModel, EntryModel, MatchEntry, MatchGroupModel, MatchModel, MatchRequest, MatchResponse, } from "./matches";
18
+ export { describeMatches, findMatches } from "./matches";
19
+ export { regex101Url } from "./extlinks";
20
+ export type { ErrorModel, ExplanationModel, ExplanationRow, FlagModel, RenderOptions, WarningModel, } from "./render";
21
+ export { ExplanationRenderer } from "./render";
22
+ export type { TextPart } from "./text";