regex-explain 0.0.0 → 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,1207 @@
1
+ // src/explainer.ts
2
+ import { RegExpParser, RegExpSyntaxError } from "@eslint-community/regexpp";
3
+ import {
4
+ getCapturingGroupNumber,
5
+ getLengthRange,
6
+ toUnicodeSet
7
+ } from "regexp-ast-analysis";
8
+ import { analyse } from "scslre";
9
+
10
+ // src/text.ts
11
+ var NAMED_CODE_POINTS = {
12
+ 0: "a null character",
13
+ 8: "a backspace",
14
+ 9: "a tab",
15
+ 10: "a line feed (newline)",
16
+ 11: "a vertical tab",
17
+ 12: "a form feed",
18
+ 13: "a carriage return",
19
+ 32: "a space",
20
+ 160: "a non-breaking space",
21
+ 8232: "a line separator",
22
+ 8233: "a paragraph separator",
23
+ 65279: "a byte order mark"
24
+ };
25
+ function code(text) {
26
+ const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((m) => m.length));
27
+ const fence = "`".repeat(longest + 1);
28
+ const pad = text.startsWith("`") || text.endsWith("`") || /^ .* $/.test(text) ? " " : "";
29
+ return `${fence}${pad}${text}${pad}${fence}`;
30
+ }
31
+ function describeCodePoint(cp) {
32
+ const named = NAMED_CODE_POINTS[cp];
33
+ if (named) {
34
+ return named;
35
+ }
36
+ const ch = String.fromCodePoint(cp);
37
+ if (/^[\p{C}\p{Z}]$/u.test(ch)) {
38
+ return `the character U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
39
+ }
40
+ return `the character ${code(ch)}`;
41
+ }
42
+ function lowerFirst(text) {
43
+ return text.charAt(0).toLowerCase() + text.slice(1);
44
+ }
45
+ function withArticle(noun) {
46
+ return `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
47
+ }
48
+ function splitCodeSpans(text) {
49
+ const spans = [...text.matchAll(/(?<!`)(`+)(?!`)([\s\S]+?)(?<!`)\1(?!`)/g)];
50
+ const plainStarts = [0, ...spans.map((span) => span.index + span[0].length)];
51
+ const parts = spans.flatMap((span, i) => {
52
+ const content = span[2];
53
+ const padded = content.length > 2 && content.startsWith(" ") && content.endsWith(" ");
54
+ return [
55
+ ...plainPart(text.slice(plainStarts[i], span.index)),
56
+ { text: padded ? content.slice(1, -1) : content, code: true }
57
+ ];
58
+ });
59
+ return [...parts, ...plainPart(text.slice(plainStarts[spans.length]))];
60
+ }
61
+ function plainPart(text) {
62
+ return text === "" ? [] : [{ text, code: false }];
63
+ }
64
+
65
+ // src/explainer.ts
66
+ function isRegexSource(value) {
67
+ const candidate = value;
68
+ return typeof candidate?.pattern === "string" && typeof candidate.flags === "string";
69
+ }
70
+ var FLAG_INFO = {
71
+ d: { name: "hasIndices", description: "Record start and end indices for each capture group." },
72
+ g: { name: "global", description: "Find all matches instead of stopping after the first." },
73
+ i: { name: "ignoreCase", description: "Ignore case: letters match both upper and lower case." },
74
+ m: {
75
+ name: "multiline",
76
+ description: `${code("^")} and ${code("$")} match at the start and end of each line, not just the whole input.`
77
+ },
78
+ s: { name: "dotAll", description: `${code(".")} also matches line terminators.` },
79
+ u: {
80
+ name: "unicode",
81
+ description: "Treat the pattern as a sequence of Unicode code points and use strict syntax."
82
+ },
83
+ v: {
84
+ name: "unicodeSets",
85
+ description: `Unicode mode with set operations (${code("--")}, ${code("&&")}), nested classes, and string properties.`
86
+ },
87
+ y: { name: "sticky", description: `Match only at the position given by ${code("lastIndex")}.` }
88
+ };
89
+ var UNICODE_PROPERTY_NAMES = {
90
+ L: "letter",
91
+ Letter: "letter",
92
+ Lu: "uppercase letter",
93
+ Uppercase_Letter: "uppercase letter",
94
+ Ll: "lowercase letter",
95
+ Lowercase_Letter: "lowercase letter",
96
+ Lt: "titlecase letter",
97
+ Lm: "modifier letter",
98
+ Lo: "other letter",
99
+ M: "mark",
100
+ Mark: "mark",
101
+ N: "number",
102
+ Number: "number",
103
+ Nd: "decimal digit",
104
+ Decimal_Number: "decimal digit",
105
+ P: "punctuation character",
106
+ Punctuation: "punctuation character",
107
+ S: "symbol",
108
+ Symbol: "symbol",
109
+ Sc: "currency symbol",
110
+ Currency_Symbol: "currency symbol",
111
+ Z: "separator",
112
+ Separator: "separator",
113
+ Zs: "space separator",
114
+ Space_Separator: "space separator",
115
+ C: "control or other character",
116
+ Other: "control or other character",
117
+ Cc: "control character",
118
+ Control: "control character",
119
+ Alphabetic: "alphabetic character",
120
+ Alpha: "alphabetic character",
121
+ White_Space: "whitespace character",
122
+ space: "whitespace character",
123
+ Emoji: "emoji character",
124
+ ASCII: "ASCII character",
125
+ Any: "character"
126
+ };
127
+ var SCRIPT_KEYS = /* @__PURE__ */ new Set(["Script", "sc", "Script_Extensions", "scx"]);
128
+ function explainRegex(pattern, flags = "", options = {}) {
129
+ const parser = new RegExpParser({ ecmaVersion: 2025 });
130
+ let flagsAst;
131
+ let patternAst;
132
+ try {
133
+ flagsAst = parser.parseFlags(flags);
134
+ patternAst = parser.parsePattern(pattern, 0, pattern.length, {
135
+ unicode: flagsAst.unicode,
136
+ unicodeSets: flagsAst.unicodeSets
137
+ });
138
+ } catch (error) {
139
+ return {
140
+ ok: false,
141
+ pattern,
142
+ flags,
143
+ message: error instanceof Error ? error.message : String(error),
144
+ index: error instanceof RegExpSyntaxError ? error.index : void 0
145
+ };
146
+ }
147
+ const ctx = { flags: flagsAst };
148
+ const length = getLengthRange(patternAst.alternatives, flagsAst);
149
+ return {
150
+ ok: true,
151
+ pattern,
152
+ flags,
153
+ nodes: explainAlternatives(patternAst.alternatives, ctx),
154
+ flagExplanations: [...flags].map((flag) => ({ flag, ...FLAG_INFO[flag] })),
155
+ length: { min: length.min, max: length.max },
156
+ warnings: options.checkBacktracking === false ? [] : findBacktracking(patternAst, flagsAst, pattern),
157
+ multiline: flagsAst.multiline || enablesMultiline(patternAst)
158
+ };
159
+ }
160
+ function enablesMultiline(node) {
161
+ if (node.type === "Group" && node.modifiers?.add.multiline) {
162
+ return true;
163
+ }
164
+ const children = "alternatives" in node ? node.alternatives : node.type === "Alternative" ? node.elements : node.type === "Quantifier" ? [node.element] : [];
165
+ return children.some(enablesMultiline);
166
+ }
167
+ function parseRegexInput(text) {
168
+ const literal2 = /^\/((?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\[])+)\/([a-z]*)$/s.exec(text.trim());
169
+ return literal2 ? { pattern: literal2[1], flags: literal2[2] } : { pattern: text, flags: "" };
170
+ }
171
+ function findBacktracking(pattern, flags, source) {
172
+ const { reports } = analyse({ pattern, flags }, { reportTypes: { Move: false } });
173
+ return reports.map((report) => toWarning(report, source));
174
+ }
175
+ function toWarning(report, source) {
176
+ const quant = report.type === "Trade" ? report.startQuant : report.quant;
177
+ const cost = `so a failed match can take ${report.exponential ? "exponential" : "polynomial"} time`;
178
+ let description;
179
+ switch (report.type) {
180
+ case "Trade":
181
+ description = `${code(report.startQuant.raw)} and ${code(report.endQuant.raw)} can match the same text, ${cost}`;
182
+ break;
183
+ case "Self":
184
+ description = `${code(report.quant.raw)} is repeated by ${code(report.parentQuant.raw)} and can match the same text in many ways, ${cost}`;
185
+ break;
186
+ case "Move":
187
+ description = `${code(report.quant.raw)} is retried at every position of a failing input, ${cost}`;
188
+ break;
189
+ }
190
+ const fixed = report.fix();
191
+ return {
192
+ raw: source.slice(quant.start, quant.end),
193
+ start: quant.start,
194
+ end: quant.end,
195
+ exponential: report.exponential,
196
+ attackCharacter: report.character.pick,
197
+ description,
198
+ fix: fixed ? { pattern: fixed.source, flags: fixed.flags } : void 0
199
+ };
200
+ }
201
+ function make(span, description, children = []) {
202
+ return { raw: span.raw, start: span.start, end: span.end, description, children };
203
+ }
204
+ function prefixed(prefix, node) {
205
+ return { ...node, description: prefix + node.description };
206
+ }
207
+ function explainAlternatives(alternatives, ctx) {
208
+ if (alternatives.length === 1) {
209
+ return explainSequence(alternatives[0], ctx);
210
+ }
211
+ const options = alternatives.map((alt, i) => {
212
+ const steps = explainSequence(alt, ctx);
213
+ if (steps.length === 1) {
214
+ return make(alt, `Option ${i + 1}: ${lowerFirst(steps[0].description)}`, [
215
+ ...steps[0].children
216
+ ]);
217
+ }
218
+ return make(alt, `Option ${i + 1}:`, steps);
219
+ });
220
+ const span = {
221
+ raw: alternatives.map((a) => a.raw).join("|"),
222
+ start: alternatives[0].start,
223
+ end: alternatives[alternatives.length - 1].end
224
+ };
225
+ return [make(span, `Match one of ${alternatives.length} alternatives:`, options)];
226
+ }
227
+ function explainSequence(alt, ctx) {
228
+ if (alt.elements.length === 0) {
229
+ return [make(alt, "Match the empty string")];
230
+ }
231
+ const steps = alt.elements.reduce((acc, el) => {
232
+ const last = acc[acc.length - 1];
233
+ if (el.type !== "Character") {
234
+ return [...acc, el];
235
+ }
236
+ return Array.isArray(last) ? [...acc.slice(0, -1), [...last, el]] : [...acc, [el]];
237
+ }, []);
238
+ return steps.map(
239
+ (step) => Array.isArray(step) ? explainCharacterRun(step, ctx) : explainElement(step, ctx)
240
+ );
241
+ }
242
+ function caseNote(elements, ctx) {
243
+ if (!ctx.flags.ignoreCase) {
244
+ return "";
245
+ }
246
+ const sensitive = { ...ctx.flags, ignoreCase: false };
247
+ const changes = elements.some(
248
+ (el) => toUnicodeSet(el, ctx.flags).chars.size !== toUnicodeSet(el, sensitive).chars.size
249
+ );
250
+ return changes ? " (case-insensitive)" : "";
251
+ }
252
+ function explainCharacterRun(run, ctx) {
253
+ const note = caseNote(run, ctx);
254
+ if (run.length === 1) {
255
+ return make(run[0], `Match ${describeCodePoint(run[0].value)}${note}`);
256
+ }
257
+ const text = String.fromCodePoint(...run.map((c) => c.value));
258
+ const span = {
259
+ raw: run.map((c) => c.raw).join(""),
260
+ start: run[0].start,
261
+ end: run[run.length - 1].end
262
+ };
263
+ if (/^[^\p{C}\s](?:[^\p{C}]*[^\p{C}\s])?$/u.test(text)) {
264
+ return make(span, `Match the text ${code(text)}${note}`);
265
+ }
266
+ const names = run.map((c) => describeCodePoint(c.value)).join(", ");
267
+ return make(span, `Match the sequence ${names}${note}`);
268
+ }
269
+ function explainElement(el, ctx) {
270
+ switch (el.type) {
271
+ case "Assertion":
272
+ return explainAssertion(el, ctx);
273
+ case "Quantifier":
274
+ return explainQuantifier(el, ctx);
275
+ default:
276
+ return explainQuantifiable(el, ctx);
277
+ }
278
+ }
279
+ function explainQuantifiable(el, ctx) {
280
+ switch (el.type) {
281
+ case "Character":
282
+ return explainCharacterRun([el], ctx);
283
+ case "CapturingGroup": {
284
+ const n = getCapturingGroupNumber(el);
285
+ const label = el.name ? `Capture group #${n} named ${code(el.name)}` : `Capture group #${n}`;
286
+ return make(el, `${label}:`, explainAlternatives(el.alternatives, ctx));
287
+ }
288
+ case "Group": {
289
+ if (!el.modifiers) {
290
+ return make(el, "Group (non-capturing):", explainAlternatives(el.alternatives, ctx));
291
+ }
292
+ const inner = applyModifiers(el.modifiers, ctx);
293
+ const label = `Group with modifiers (${describeModifiers(el.modifiers)}):`;
294
+ return make(el, label, explainAlternatives(el.alternatives, inner));
295
+ }
296
+ case "Backreference":
297
+ return make(el, `Match the same text last captured by ${describeBackrefTarget(el)}`);
298
+ case "Assertion":
299
+ return explainAssertion(el, ctx);
300
+ case "CharacterClass":
301
+ case "ExpressionCharacterClass":
302
+ return explainClass(el, ctx, true);
303
+ case "CharacterSet":
304
+ return make(el, `Match ${describeCharacterSet(el, ctx)}${caseNote([el], ctx)}`);
305
+ }
306
+ }
307
+ function explainQuantifier(q, ctx) {
308
+ const inner = explainQuantifiable(q.element, ctx);
309
+ const greed = q.min === q.max ? "" : q.greedy ? " (as many as possible)" : " (as few as possible)";
310
+ const repetition = `${describeCount(q.min, q.max)}${greed}`;
311
+ if (inner.children.length === 0) {
312
+ return make(q, `${inner.description}, ${repetition}`);
313
+ }
314
+ const heading = inner.description.replace(/:$/, "");
315
+ return make(q, `${heading}, ${repetition}:`, [...inner.children]);
316
+ }
317
+ function describeCount(min, max) {
318
+ if (min === 0 && max === 1) {
319
+ return "optionally";
320
+ }
321
+ if (max === Infinity) {
322
+ return min === 0 ? "zero or more times" : min === 1 ? "one or more times" : `${min} or more times`;
323
+ }
324
+ if (min === max) {
325
+ return min === 1 ? "exactly once" : `exactly ${min} times`;
326
+ }
327
+ return `between ${min} and ${max} times`;
328
+ }
329
+ function explainAssertion(a, ctx) {
330
+ const where = ctx.flags.multiline ? "a line" : "the input";
331
+ switch (a.kind) {
332
+ case "start":
333
+ return make(a, `Assert position is at the start of ${where}`);
334
+ case "end":
335
+ return make(a, `Assert position is at the end of ${where}`);
336
+ case "word":
337
+ return make(a, `Assert position is ${a.negate ? "not " : ""}at a word boundary`);
338
+ case "lookahead":
339
+ return make(
340
+ a,
341
+ a.negate ? "Negative lookahead: assert that what follows does not match:" : "Lookahead: assert that what follows matches:",
342
+ explainAlternatives(a.alternatives, ctx)
343
+ );
344
+ case "lookbehind":
345
+ return make(
346
+ a,
347
+ a.negate ? "Negative lookbehind: assert that what precedes does not match:" : "Lookbehind: assert that what precedes matches:",
348
+ explainAlternatives(a.alternatives, ctx)
349
+ );
350
+ }
351
+ }
352
+ function describeBackrefTarget(ref) {
353
+ if (ref.ambiguous) {
354
+ const numbers = ref.resolved.map((g) => `#${getCapturingGroupNumber(g)}`).join(" or ");
355
+ return `the group named ${code(ref.ref)} (${numbers})`;
356
+ }
357
+ const n = getCapturingGroupNumber(ref.resolved);
358
+ return typeof ref.ref === "string" ? `group #${n} (${code(ref.ref)})` : `group #${n}`;
359
+ }
360
+ function applyModifiers(m, ctx) {
361
+ const flag = (name) => m.remove?.[name] ? false : m.add?.[name] ? true : ctx.flags[name];
362
+ return {
363
+ flags: {
364
+ ...ctx.flags,
365
+ ignoreCase: flag("ignoreCase"),
366
+ multiline: flag("multiline"),
367
+ dotAll: flag("dotAll")
368
+ }
369
+ };
370
+ }
371
+ function describeModifiers(m) {
372
+ const names = (f) => f ? [
373
+ f.ignoreCase ? "ignore case" : "",
374
+ f.multiline ? "multiline" : "",
375
+ f.dotAll ? "dotAll" : ""
376
+ ].filter(Boolean) : [];
377
+ const on = names(m.add);
378
+ const off = names(m.remove);
379
+ return [on.length ? `enable ${on.join(", ")}` : "", off.length ? `disable ${off.join(", ")}` : ""].filter(Boolean).join("; ");
380
+ }
381
+ function explainClass(cls, ctx, topLevel) {
382
+ const verb = topLevel ? "Match " : "";
383
+ const note = topLevel ? caseNote([cls], ctx) : "";
384
+ if (cls.type === "ExpressionCharacterClass") {
385
+ const what2 = cls.negate ? "any character not in" : "a character in";
386
+ return make(cls, `${verb}${what2} the set${note}:`, [explainClassElement(cls.expression, ctx)]);
387
+ }
388
+ if (cls.elements.length === 0) {
389
+ return make(
390
+ cls,
391
+ cls.negate ? `${verb}any character at all` : `${verb}nothing (an empty class never matches)`
392
+ );
393
+ }
394
+ const what = cls.negate ? "any character except" : "one of";
395
+ return make(
396
+ cls,
397
+ `${verb}${what}${note}:`,
398
+ cls.elements.map((e) => explainClassElement(e, ctx))
399
+ );
400
+ }
401
+ function explainClassElement(el, ctx) {
402
+ switch (el.type) {
403
+ case "Character":
404
+ return make(el, describeCodePoint(el.value));
405
+ case "CharacterClassRange":
406
+ return make(
407
+ el,
408
+ `a character from ${describeCodePoint(el.min.value)} to ${describeCodePoint(el.max.value)}`
409
+ );
410
+ case "CharacterSet":
411
+ return make(el, describeCharacterSet(el, ctx));
412
+ case "CharacterClass":
413
+ case "ExpressionCharacterClass":
414
+ return explainClass(el, ctx, false);
415
+ case "ClassStringDisjunction": {
416
+ const strings = el.alternatives.map(
417
+ (a) => a.elements.length ? code(String.fromCodePoint(...a.elements.map((c) => c.value))) : "the empty string"
418
+ );
419
+ return make(
420
+ el,
421
+ strings.length === 1 ? `the string ${strings[0]}` : `one of the strings ${strings.join(", ")}`
422
+ );
423
+ }
424
+ case "ClassIntersection":
425
+ return make(
426
+ el,
427
+ "characters that are in all of:",
428
+ flattenOperands(el).map((o) => explainClassElement(o, ctx))
429
+ );
430
+ case "ClassSubtraction": {
431
+ const [first, ...rest] = flattenOperands(el);
432
+ return make(el, "characters that are:", [
433
+ prefixed("in ", explainClassElement(first, ctx)),
434
+ ...rest.map((o) => prefixed("but not ", explainClassElement(o, ctx)))
435
+ ]);
436
+ }
437
+ }
438
+ }
439
+ function flattenOperands(node) {
440
+ const left = node.left.type === node.type ? flattenOperands(node.left) : [node.left];
441
+ return [...left, node.right];
442
+ }
443
+ function describeCharacterSet(set, ctx) {
444
+ switch (set.kind) {
445
+ case "any":
446
+ return ctx.flags.dotAll ? "any character" : "any character except a line break";
447
+ case "digit":
448
+ return set.negate ? "any character that is not a digit" : "a digit (0-9)";
449
+ case "space":
450
+ return set.negate ? "any character that is not whitespace" : "a whitespace character (space, tab, line break, etc.)";
451
+ case "word":
452
+ return set.negate ? "any character that is not a word character" : "a word character (letter, digit, or underscore)";
453
+ case "property":
454
+ return describeProperty(set);
455
+ }
456
+ }
457
+ function describeProperty(set) {
458
+ const raw = set.value ? `${set.key}=${set.value}` : set.key;
459
+ if (set.strings) {
460
+ return `a string with Unicode property ${code(raw)}`;
461
+ }
462
+ const name = set.value ?? set.key;
463
+ const friendly = set.value && SCRIPT_KEYS.has(set.key) ? `character in the ${set.value.replace(/_/g, " ")} script` : UNICODE_PROPERTY_NAMES[name] ?? `character with Unicode property ${code(raw)}`;
464
+ return set.negate ? `any character that is not ${withArticle(friendly)}` : withArticle(friendly);
465
+ }
466
+
467
+ // src/compiler.ts
468
+ var JS_STRING = "[(string) (template_string)]";
469
+ var JS_QUERY = `
470
+ (regex pattern: (regex_pattern) @pattern flags: (regex_flags)? @flags) @regex
471
+ (new_expression
472
+ constructor: (identifier) @_constructor
473
+ arguments: (arguments . ${JS_STRING} @pattern.string . ${JS_STRING}? @flags.string)
474
+ (#eq? @_constructor "RegExp"))
475
+ (call_expression
476
+ function: (identifier) @_constructor
477
+ arguments: (arguments . ${JS_STRING} @pattern.string . ${JS_STRING}? @flags.string)
478
+ (#eq? @_constructor "RegExp"))
479
+ `;
480
+ var CS_STRING = "[(string_literal) (verbatim_string_literal) (raw_string_literal)]";
481
+ var RUBY_FLAGS = { m: "s", o: "", n: "", e: "", s: "", u: "" };
482
+ var PHP_FLAGS = { u: "", S: "" };
483
+ var LANGUAGES = {
484
+ javascript: { grammar: "javascript", query: JS_QUERY },
485
+ javascriptreact: { grammar: "javascript", query: JS_QUERY },
486
+ typescript: { grammar: "typescript", query: JS_QUERY },
487
+ typescriptreact: { grammar: "tsx", query: JS_QUERY },
488
+ vue: { grammar: "typescript", query: JS_QUERY, scriptBlocks: true },
489
+ svelte: { grammar: "typescript", query: JS_QUERY, scriptBlocks: true },
490
+ python: {
491
+ grammar: "python",
492
+ query: `
493
+ (call
494
+ function: (attribute object: (identifier) @_module attribute: (identifier) @_function)
495
+ arguments: (argument_list . (string) @pattern.string)
496
+ (#any-of? @_module "re" "regex")
497
+ (#any-of? @_function
498
+ "compile" "search" "match" "fullmatch" "split" "findall" "finditer" "sub" "subn"))
499
+ `
500
+ },
501
+ go: {
502
+ grammar: "go",
503
+ query: `
504
+ (call_expression
505
+ function: (selector_expression operand: (identifier) @_package field: (field_identifier) @_function)
506
+ arguments: (argument_list
507
+ . [(raw_string_literal) (interpreted_string_literal)] @pattern.string)
508
+ (#eq? @_package "regexp")
509
+ (#any-of? @_function
510
+ "Compile" "CompilePOSIX" "MustCompile" "MustCompilePOSIX" "Match" "MatchReader" "MatchString"))
511
+ `
512
+ },
513
+ rust: {
514
+ grammar: "rust",
515
+ query: `
516
+ (call_expression
517
+ function: (scoped_identifier path: (_) @_type name: (identifier) @_function)
518
+ arguments: (arguments . [(string_literal) (raw_string_literal)] @pattern.string)
519
+ (#match? @_type "(^|::)(Regex|RegexBuilder)$")
520
+ (#eq? @_function "new"))
521
+ `
522
+ },
523
+ java: {
524
+ // Text blocks ("""...""") strip indentation, which the decoder does not model.
525
+ grammar: "java",
526
+ query: `
527
+ (method_invocation
528
+ object: (identifier) @_class
529
+ name: (identifier) @_method
530
+ arguments: (argument_list . (string_literal) @pattern.string)
531
+ (#eq? @_class "Pattern")
532
+ (#eq? @_method "compile")
533
+ (#not-match? @pattern.string "^\\"\\"\\""))
534
+ (method_invocation
535
+ name: (identifier) @_method
536
+ arguments: (argument_list . (string_literal) @pattern.string)
537
+ (#any-of? @_method "matches" "split")
538
+ (#not-match? @pattern.string "^\\"\\"\\""))
539
+ (method_invocation
540
+ name: (identifier) @_method
541
+ arguments: (argument_list . (string_literal) @pattern.string . (_))
542
+ (#any-of? @_method "replaceAll" "replaceFirst")
543
+ (#not-match? @pattern.string "^\\"\\"\\""))
544
+ `
545
+ },
546
+ csharp: {
547
+ // Multi-line raw strings strip indentation, which the decoder does not model.
548
+ grammar: "c-sharp",
549
+ query: `
550
+ (object_creation_expression
551
+ type: (_) @_type
552
+ arguments: (argument_list . (argument ${CS_STRING} @pattern.string))
553
+ (#match? @_type "(^|\\\\.)Regex$")
554
+ (#not-match? @pattern.string "^\\"\\"\\"+\\\\s*\\n"))
555
+ (invocation_expression
556
+ function: (member_access_expression expression: (_) @_type name: (identifier) @_method)
557
+ arguments: (argument_list . (argument) . (argument ${CS_STRING} @pattern.string))
558
+ (#match? @_type "(^|\\\\.)Regex$")
559
+ (#any-of? @_method "Count" "EnumerateMatches" "IsMatch" "Match" "Matches" "Replace" "Split")
560
+ (#not-match? @pattern.string "^\\"\\"\\"+\\\\s*\\n"))
561
+ (attribute
562
+ name: (identifier) @_attribute
563
+ (attribute_argument_list . (attribute_argument ${CS_STRING} @pattern.string))
564
+ (#eq? @_attribute "GeneratedRegex")
565
+ (#not-match? @pattern.string "^\\"\\"\\"+\\\\s*\\n"))
566
+ `
567
+ },
568
+ ruby: {
569
+ grammar: "ruby",
570
+ flags: RUBY_FLAGS,
571
+ query: `
572
+ (regex) @literal
573
+ (call
574
+ receiver: (constant) @_class
575
+ method: (identifier) @_method
576
+ arguments: (argument_list . (string) @pattern.string)
577
+ (#eq? @_class "Regexp")
578
+ (#any-of? @_method "new" "compile"))
579
+ `
580
+ },
581
+ php: {
582
+ grammar: "php",
583
+ flags: PHP_FLAGS,
584
+ query: `
585
+ (function_call_expression
586
+ function: (name) @_function
587
+ arguments: (arguments . (argument [(string) (encapsed_string)] @pattern.delimited))
588
+ (#match? @_function "^preg_(filter|grep|match|match_all|replace|replace_callback|split)$"))
589
+ `
590
+ }
591
+ };
592
+ var SUPPORTED_LANGUAGE_IDS = Object.keys(LANGUAGES);
593
+ var GRAMMARS = [
594
+ ...new Set(Object.values(LANGUAGES).map((spec) => spec.grammar))
595
+ ];
596
+
597
+ // src/strings.ts
598
+ var SIMPLE_ESCAPES = {
599
+ b: "\b",
600
+ f: "\f",
601
+ n: "\n",
602
+ r: "\r",
603
+ t: " ",
604
+ v: "\v"
605
+ };
606
+ var CLOSING_BRACKETS = {
607
+ "(": ")",
608
+ "[": "]",
609
+ "{": "}",
610
+ "<": ">"
611
+ };
612
+ function decodeEscape(escape) {
613
+ const body = escape.slice(1);
614
+ const hex = /^(?:x([\da-f]{1,2})|[xu]\{([\da-f]+)\}|u([\da-f]{4})|U([\da-f]{8}))$/i.exec(body);
615
+ if (hex) {
616
+ const codePoint = parseInt(hex.slice(1).find((group) => group !== void 0) ?? "", 16);
617
+ return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : void 0;
618
+ }
619
+ if (/^[0-7]{1,3}$/.test(body)) {
620
+ return String.fromCodePoint(parseInt(body, 8));
621
+ }
622
+ if (/^\r?\n$/.test(body)) {
623
+ return "";
624
+ }
625
+ if ([...body].length !== 1 || body === "a" || body === "e") {
626
+ return void 0;
627
+ }
628
+ return SIMPLE_ESCAPES[body] ?? body;
629
+ }
630
+ function splitDelimitedPattern(value) {
631
+ const open = value.charAt(0);
632
+ if (open === "" || /[\p{L}\p{N}\s\\]/u.test(open)) {
633
+ return void 0;
634
+ }
635
+ const close = value.lastIndexOf(CLOSING_BRACKETS[open] ?? open);
636
+ if (close < 1) {
637
+ return void 0;
638
+ }
639
+ return { pattern: value.slice(1, close), flags: value.slice(close + 1) };
640
+ }
641
+ function translateFlags(flags, map) {
642
+ return [...flags].map((flag) => map[flag] ?? flag).join("");
643
+ }
644
+
645
+ // src/locator.ts
646
+ var MAX_SCAN_LENGTH = 1e6;
647
+ var CONTENT_TYPE = /(?:content|fragment)$/;
648
+ var DELIMITER_TYPE = /(?:_start|_end)$/;
649
+ var SCRIPT_BLOCK = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
650
+ var runtime;
651
+ async function loadRuntime(wasmDir) {
652
+ const imported = await import("@vscode/tree-sitter-wasm");
653
+ const treeSitter = imported.default ?? imported;
654
+ await treeSitter.Parser.init({ locateFile: (file) => joinPath(wasmDir, file) });
655
+ return treeSitter;
656
+ }
657
+ function joinPath(dir, file) {
658
+ return `${dir.replace(/[\\/]+$/, "")}/${file}`;
659
+ }
660
+ var RegexFinder = class {
661
+ wasmDir;
662
+ grammars = /* @__PURE__ */ new Map();
663
+ languages = /* @__PURE__ */ new Map();
664
+ /**
665
+ * Constructs a new RegexFinder.
666
+ *
667
+ * @param wasmDir Directory or URL holding `tree-sitter.wasm` and the `tree-sitter-<grammar>.wasm`
668
+ * files, such as the `wasm` directory of `@vscode/tree-sitter-wasm`.
669
+ */
670
+ constructor(wasmDir) {
671
+ this.wasmDir = wasmDir;
672
+ }
673
+ /**
674
+ * Checks whether regexes can be found in a language.
675
+ *
676
+ * @param languageId A VS Code language ID.
677
+ *
678
+ * @returns Whether the language has a {@link LanguageSpec}.
679
+ */
680
+ supports(languageId) {
681
+ return Object.hasOwn(LANGUAGES, languageId);
682
+ }
683
+ /**
684
+ * Lists the regexes in source text, in order.
685
+ *
686
+ * @param source The source text.
687
+ * @param languageId The VS Code language ID of the source.
688
+ *
689
+ * @returns The regexes found; none for unsupported languages or for sources longer than
690
+ * {@link MAX_SCAN_LENGTH}. Patterns built at run time, such as strings with interpolations, are
691
+ * skipped.
692
+ */
693
+ async find(source, languageId) {
694
+ if (source.length > MAX_SCAN_LENGTH || !this.supports(languageId)) {
695
+ return [];
696
+ }
697
+ const spec = LANGUAGES[languageId];
698
+ const { parser, query } = await this.language(languageId, spec);
699
+ const includedRanges = spec.scriptBlocks ? scriptRanges(source) : void 0;
700
+ if (includedRanges?.length === 0) {
701
+ return [];
702
+ }
703
+ const tree = parser.parse(source, null, { includedRanges });
704
+ if (!tree) {
705
+ return [];
706
+ }
707
+ try {
708
+ const regexes = query.matches(tree.rootNode).flatMap((match) => readMatch(match.captures, source, spec) ?? []);
709
+ const byStart = new Map([...regexes].reverse().map((regex) => [regex.start, regex]));
710
+ return [...byStart.values()].sort((a, b) => a.start - b.start);
711
+ } finally {
712
+ tree.delete();
713
+ }
714
+ }
715
+ /**
716
+ * Creates a parser and compiles the query for a language, once per language.
717
+ *
718
+ * @param languageId The VS Code language ID.
719
+ * @param spec The language's entry in {@link LANGUAGES}.
720
+ *
721
+ * @returns The parser and query.
722
+ */
723
+ language(languageId, spec) {
724
+ let loaded = this.languages.get(languageId);
725
+ if (!loaded) {
726
+ loaded = Promise.all([this.runtime(), this.grammar(spec.grammar)]).then(
727
+ ([treeSitter, language]) => {
728
+ const parser = new treeSitter.Parser();
729
+ parser.setLanguage(language);
730
+ return { parser, query: new treeSitter.Query(language, spec.query) };
731
+ }
732
+ );
733
+ this.languages.set(languageId, loaded);
734
+ }
735
+ return loaded;
736
+ }
737
+ /**
738
+ * Loads a grammar from the WASM directory, once per grammar.
739
+ *
740
+ * @param grammar The grammar name.
741
+ *
742
+ * @returns The grammar.
743
+ */
744
+ grammar(grammar) {
745
+ let loaded = this.grammars.get(grammar);
746
+ if (!loaded) {
747
+ loaded = this.runtime().then(
748
+ (treeSitter) => treeSitter.Language.load(joinPath(this.wasmDir, `tree-sitter-${grammar}.wasm`))
749
+ );
750
+ this.grammars.set(grammar, loaded);
751
+ }
752
+ return loaded;
753
+ }
754
+ /**
755
+ * Loads the tree-sitter runtime, once per process.
756
+ *
757
+ * @returns The runtime's exports.
758
+ */
759
+ runtime() {
760
+ runtime ??= loadRuntime(this.wasmDir);
761
+ return runtime;
762
+ }
763
+ };
764
+ function findRegexAt(regexes, offset) {
765
+ return regexes.find((r) => offset >= r.start && offset < r.end);
766
+ }
767
+ function readMatch(captures, source, spec) {
768
+ const node = (name) => captures.find((c) => c.name === name)?.node;
769
+ const literal2 = node("literal");
770
+ const delimited = node("pattern.delimited");
771
+ const patternString = node("pattern.string");
772
+ const flagsString = node("flags.string");
773
+ const patternNode = node("pattern");
774
+ let parts;
775
+ if (literal2) {
776
+ parts = literalParts(literal2, source);
777
+ } else if (delimited) {
778
+ const value = stringValue(delimited, source);
779
+ parts = value === void 0 ? void 0 : splitDelimitedPattern(value);
780
+ } else if (patternString) {
781
+ const pattern = stringValue(patternString, source);
782
+ const flags = flagsString ? stringValue(flagsString, source) : "";
783
+ parts = pattern === void 0 || flags === void 0 ? void 0 : { pattern, flags };
784
+ } else if (patternNode) {
785
+ parts = { pattern: patternNode.text, flags: node("flags")?.text ?? "" };
786
+ }
787
+ const range = node("regex") ?? literal2 ?? delimited ?? patternString ?? patternNode;
788
+ if (!parts || !range) {
789
+ return void 0;
790
+ }
791
+ return {
792
+ pattern: parts.pattern,
793
+ flags: translateFlags(parts.flags, spec.flags ?? {}),
794
+ start: range.startIndex,
795
+ end: range.endIndex
796
+ };
797
+ }
798
+ function literalParts(node, source) {
799
+ const open = node.firstChild;
800
+ const close = node.lastChild;
801
+ if (!open || !close || open.startIndex === close.startIndex || node.namedChildren.some(isCode)) {
802
+ return void 0;
803
+ }
804
+ return {
805
+ pattern: source.slice(open.endIndex, close.startIndex),
806
+ flags: close.text.slice(1)
807
+ };
808
+ }
809
+ function stringValue(node, source) {
810
+ if (node.childCount === 0) {
811
+ return verbatimValue(node.text);
812
+ }
813
+ let value = "";
814
+ for (const child of node.children) {
815
+ if (!child || !child.isNamed || DELIMITER_TYPE.test(child.type)) {
816
+ continue;
817
+ }
818
+ const piece = child.type === "escape_sequence" ? decodeEscape(child.text) : CONTENT_TYPE.test(child.type) ? contentValue(child, source) : void 0;
819
+ if (piece === void 0) {
820
+ return void 0;
821
+ }
822
+ value += piece;
823
+ }
824
+ return value;
825
+ }
826
+ function contentValue(node, source) {
827
+ let value = "";
828
+ let offset = node.startIndex;
829
+ for (const escape of node.descendantsOfType("escape_sequence")) {
830
+ const decoded = escape ? decodeEscape(escape.text) : void 0;
831
+ if (!escape || decoded === void 0) {
832
+ return void 0;
833
+ }
834
+ value += source.slice(offset, escape.startIndex) + decoded;
835
+ offset = escape.endIndex;
836
+ }
837
+ return value + source.slice(offset, node.endIndex);
838
+ }
839
+ function verbatimValue(text) {
840
+ const quoted = /^[^"'`]*(["'`])([\s\S]*)\1$/.exec(text);
841
+ return quoted ? quoted[2].replaceAll(quoted[1] + quoted[1], quoted[1]) : void 0;
842
+ }
843
+ function isCode(node) {
844
+ return !!node && node.type !== "escape_sequence" && !CONTENT_TYPE.test(node.type) && !DELIMITER_TYPE.test(node.type);
845
+ }
846
+ function scriptRanges(source) {
847
+ return [...source.matchAll(SCRIPT_BLOCK)].map((block) => {
848
+ const startIndex = (block.index ?? 0) + block[0].indexOf(">") + 1;
849
+ const endIndex = startIndex + block[1].length;
850
+ return {
851
+ startIndex,
852
+ endIndex,
853
+ startPosition: pointAt(source, startIndex),
854
+ endPosition: pointAt(source, endIndex)
855
+ };
856
+ });
857
+ }
858
+ function pointAt(source, offset) {
859
+ const before = source.slice(0, offset);
860
+ const lineStart = before.lastIndexOf("\n") + 1;
861
+ return { row: before.split("\n").length - 1, column: offset - lineStart };
862
+ }
863
+
864
+ // src/matches.ts
865
+ function findMatches(request) {
866
+ let regex;
867
+ try {
868
+ const flags = request.flags.includes("g") ? request.flags : `${request.flags}g`;
869
+ regex = new RegExp(request.pattern, flags);
870
+ } catch (error) {
871
+ return { matches: [], truncated: false, error: error.message };
872
+ }
873
+ const lineStarts = findLineStarts(request.text);
874
+ if (!request.entries) {
875
+ const found2 = [...take(request.text.matchAll(regex), request.limit + 1)];
876
+ return {
877
+ matches: found2.slice(0, request.limit).map((match) => toFoundMatch(match, 0, lineStarts)),
878
+ truncated: found2.length > request.limit
879
+ };
880
+ }
881
+ const entries = splitEntries(request.text, lineStarts);
882
+ const found = [...take(matchEntries(entries, regex), request.limit + 1)];
883
+ const kept = found.slice(0, request.limit);
884
+ const truncated = found.length > request.limit;
885
+ const lastLine = truncated ? kept[kept.length - 1]?.entry.line ?? 0 : Infinity;
886
+ return {
887
+ matches: kept.map(({ entry, match }) => toFoundMatch(match, entry.start, lineStarts)),
888
+ truncated,
889
+ entries: entries.filter((entry) => entry.line <= lastLine)
890
+ };
891
+ }
892
+ function splitEntries(text, lineStarts) {
893
+ const lines = lineStarts.map((start, i) => {
894
+ const content = text.slice(start, lineStarts[i + 1] ?? text.length).replace(/(?:\r\n|\r|\n)$/, "");
895
+ return { line: i + 1, start, end: start + content.length, text: content };
896
+ });
897
+ return text === "" || /(?:\r\n|\r|\n)$/.test(text) ? lines.slice(0, -1) : lines;
898
+ }
899
+ function* matchEntries(entries, regex) {
900
+ for (const entry of entries) {
901
+ for (const match of entry.text.matchAll(regex)) {
902
+ yield { entry, match };
903
+ }
904
+ }
905
+ }
906
+ function toFoundMatch(match, offset, lineStarts) {
907
+ const start = offset + match.index;
908
+ const end = start + match[0].length;
909
+ return {
910
+ start,
911
+ end,
912
+ text: match[0],
913
+ startLine: lineAt(lineStarts, start),
914
+ endLine: lineAt(lineStarts, Math.max(start, end - 1)),
915
+ groups: match.slice(1),
916
+ named: { ...match.groups }
917
+ };
918
+ }
919
+ function* take(items, count) {
920
+ if (count <= 0) {
921
+ return;
922
+ }
923
+ let taken = 0;
924
+ for (const item of items) {
925
+ yield item;
926
+ taken += 1;
927
+ if (taken === count) {
928
+ return;
929
+ }
930
+ }
931
+ }
932
+ function describeMatches(response) {
933
+ const matches = response.matches.map(describeMatch);
934
+ const entries = response.entries?.map((entry) => {
935
+ const inEntry = matches.filter(
936
+ (match) => match.start >= entry.start && match.start <= entry.end
937
+ );
938
+ return {
939
+ line: `L${entry.line}`,
940
+ start: entry.start,
941
+ end: entry.end,
942
+ text: entry.text === "" ? "(empty)" : entry.text,
943
+ matched: inEntry.length > 0,
944
+ matches: inEntry
945
+ };
946
+ });
947
+ const problem = !!(response.error || response.timedOut);
948
+ const shown = response.truncated ? ` (showing the first ${matches.length} matches)` : "";
949
+ const summary = response.error ? `The regex cannot run: ${response.error}` : response.timedOut ? "Matching timed out: the pattern backtracks too much on this text." : entries ? `${describeEntryCount(entries)}${shown}.` : `${describeCount2(matches.length, "match", "matches")}${response.truncated ? ` (showing the first ${matches.length})` : ""}.`;
950
+ return entries ? { summary, problem, matches, entries } : { summary, problem, matches };
951
+ }
952
+ function describeEntryCount(entries) {
953
+ if (entries.length === 0) {
954
+ return "No entries";
955
+ }
956
+ const matched = entries.filter((entry) => entry.matched).length;
957
+ const noun = entries.length === 1 ? "entry" : "entries";
958
+ return `${matched} of ${entries.length} ${noun} ${matched === 1 ? "matches" : "match"}`;
959
+ }
960
+ function describeCount2(count, one, many) {
961
+ return `${count === 0 ? "No" : count} ${count === 1 ? one : many}`;
962
+ }
963
+ function describeMatch(match) {
964
+ const groups = [
965
+ ...match.groups.map((value, i) => [`$${i + 1}`, value]),
966
+ ...Object.entries(match.named)
967
+ ];
968
+ return {
969
+ start: match.start,
970
+ end: match.end,
971
+ lines: match.startLine === match.endLine ? `L${match.startLine}` : `L${match.startLine}\u2013${match.endLine}`,
972
+ text: match.text === "" ? "(empty)" : match.text,
973
+ groups: groups.map(([name, value]) => ({
974
+ name,
975
+ value: value ?? "(no match)",
976
+ matched: value !== void 0
977
+ }))
978
+ };
979
+ }
980
+ function findLineStarts(text) {
981
+ const lineBreaks = [...text.matchAll(/\r\n|\r|\n/g)];
982
+ return [0, ...lineBreaks.map((lineBreak) => lineBreak.index + lineBreak[0].length)];
983
+ }
984
+ function lineAt(lineStarts, offset) {
985
+ let low = 0;
986
+ let high = lineStarts.length - 1;
987
+ while (low < high) {
988
+ const middle = Math.ceil((low + high) / 2);
989
+ if (lineStarts[middle] <= offset) {
990
+ low = middle;
991
+ } else {
992
+ high = middle - 1;
993
+ }
994
+ }
995
+ return low + 1;
996
+ }
997
+
998
+ // src/extlinks.ts
999
+ var REGEX101 = "https://regex101.com/";
1000
+ function regex101Url(source, testString = "") {
1001
+ const params = [
1002
+ ["regex", source.pattern],
1003
+ ["flags", source.flags],
1004
+ ["flavor", "javascript"],
1005
+ ...testString === "" ? [] : [["testString", testString]]
1006
+ ];
1007
+ const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
1008
+ return `${REGEX101}?${query}`;
1009
+ }
1010
+
1011
+ // src/render.ts
1012
+ var ExplanationRenderer = class {
1013
+ result;
1014
+ /**
1015
+ * Constructs a new ExplanationRenderer.
1016
+ *
1017
+ * @param result The result of `explainRegex`.
1018
+ */
1019
+ constructor(result) {
1020
+ this.result = result;
1021
+ }
1022
+ /**
1023
+ * Collects everything an explanation view shows. Every output format renders from this model.
1024
+ *
1025
+ * @returns The model.
1026
+ */
1027
+ model() {
1028
+ const result = this.result;
1029
+ if (!result.ok) {
1030
+ return {
1031
+ literal: literal(result),
1032
+ error: {
1033
+ message: result.message,
1034
+ pattern: result.pattern,
1035
+ index: result.index,
1036
+ caret: result.index === void 0 ? void 0 : "^".padStart(result.index + 1)
1037
+ },
1038
+ warnings: [],
1039
+ rows: [],
1040
+ flags: [],
1041
+ multiline: false
1042
+ };
1043
+ }
1044
+ return {
1045
+ literal: literal(result),
1046
+ multiline: result.multiline,
1047
+ warnings: result.warnings.map((warning) => ({
1048
+ ...described(warning.description),
1049
+ fix: warning.fix && literal(warning.fix)
1050
+ })),
1051
+ rows: flatten(result.nodes),
1052
+ length: describeLength(result),
1053
+ flags: result.flagExplanations.map((f) => ({
1054
+ flag: f.flag,
1055
+ name: f.name,
1056
+ ...described(f.description)
1057
+ }))
1058
+ };
1059
+ }
1060
+ /**
1061
+ * Renders the explanation as a nested Markdown list, suitable for hovers and documentation.
1062
+ *
1063
+ * @param options Rendering options.
1064
+ *
1065
+ * @returns The Markdown.
1066
+ */
1067
+ markdownList(options = {}) {
1068
+ return this.markdown(
1069
+ options,
1070
+ (rows) => rows.map((row) => `${" ".repeat(row.depth)}- ${code(row.raw)} ${row.description}`)
1071
+ );
1072
+ }
1073
+ /**
1074
+ * Renders the explanation as a Markdown table with a Code Point and an Explanation column. Nested
1075
+ * steps are indented with non-breaking spaces.
1076
+ *
1077
+ * @param options Rendering options.
1078
+ *
1079
+ * @returns The Markdown.
1080
+ */
1081
+ markdownTable(options = {}) {
1082
+ return this.markdown(options, (rows) => [
1083
+ "| Code Point | Explanation |",
1084
+ "| ---------- | ----------- |",
1085
+ ...rows.map(
1086
+ (row) => `| ${"&nbsp;&nbsp;".repeat(row.depth)}${escapePipes(code(row.raw))} | ${escapePipes(row.description)} |`
1087
+ )
1088
+ ]);
1089
+ }
1090
+ /**
1091
+ * Renders the explanation as indented plain text, for terminals and logs.
1092
+ *
1093
+ * @param options Rendering options.
1094
+ *
1095
+ * @returns The text.
1096
+ */
1097
+ text(options = {}) {
1098
+ const model = this.model();
1099
+ const title = options.title ? [model.literal, ""] : [];
1100
+ if (model.error) {
1101
+ const caret = model.error.caret === void 0 ? [] : [` ${model.error.pattern}`, ` ${model.error.caret}`];
1102
+ return [...title, `Invalid regular expression: ${model.error.message}`, ...caret].join("\n");
1103
+ }
1104
+ const warnings = model.warnings.flatMap((warning) => [
1105
+ `Warning: ${plain(warning.parts)}.`,
1106
+ ...warning.fix ? [` Equivalent safe pattern: ${warning.fix}`] : []
1107
+ ]);
1108
+ const width = Math.min(
1109
+ 30,
1110
+ Math.max(...model.rows.map((row) => row.depth * 2 + row.raw.length))
1111
+ );
1112
+ const steps = model.rows.map(
1113
+ (row) => `${`${" ".repeat(row.depth)}${row.raw}`.padEnd(width)} ${plain(row.parts)}`
1114
+ );
1115
+ const flags = model.flags.length === 0 ? [] : ["", "Flags:", ...model.flags.map((f) => ` ${f.flag} ${f.name}: ${plain(f.parts)}`)];
1116
+ return [
1117
+ ...title,
1118
+ ...warnings,
1119
+ ...warnings.length > 0 ? [""] : [],
1120
+ ...steps,
1121
+ "",
1122
+ model.length ?? "",
1123
+ ...flags
1124
+ ].join("\n");
1125
+ }
1126
+ /**
1127
+ * Renders the parts every Markdown format shares around its steps: the title, the error or the
1128
+ * warnings, the match length, and the flags.
1129
+ *
1130
+ * @param options Rendering options.
1131
+ * @param steps Formats the rows as Markdown lines.
1132
+ *
1133
+ * @returns The Markdown.
1134
+ */
1135
+ markdown(options, steps) {
1136
+ const model = this.model();
1137
+ const title = options.title ? ["# Regex explanation", "", code(model.literal), ""] : [];
1138
+ if (model.error) {
1139
+ const caret = model.error.caret === void 0 ? [] : ["", "```text", model.error.pattern, model.error.caret, "```"];
1140
+ const message = `**Invalid regular expression:** ${escapeMarkdown(model.error.message)}`;
1141
+ return [...title, message, ...caret].join("\n");
1142
+ }
1143
+ const warnings = model.warnings.flatMap((warning) => [
1144
+ `> **Warning:** ${warning.description}.`,
1145
+ ...warning.fix ? [`> Equivalent safe pattern: ${code(warning.fix)}`] : [],
1146
+ ""
1147
+ ]);
1148
+ const flags = model.flags.length === 0 ? [] : [
1149
+ "",
1150
+ "**Flags**",
1151
+ "",
1152
+ ...model.flags.map((f) => `- ${code(f.flag)} *${f.name}*: ${f.description}`)
1153
+ ];
1154
+ return [...title, ...warnings, ...steps(model.rows), "", `*${model.length}*`, ...flags].join(
1155
+ "\n"
1156
+ );
1157
+ }
1158
+ };
1159
+ function literal(source) {
1160
+ return `/${source.pattern}/${source.flags}`;
1161
+ }
1162
+ function described(description) {
1163
+ return { description, parts: splitCodeSpans(description) };
1164
+ }
1165
+ function flatten(nodes, depth = 0) {
1166
+ return nodes.flatMap((node) => [
1167
+ { raw: node.raw, depth, ...described(node.description) },
1168
+ ...flatten(node.children, depth + 1)
1169
+ ]);
1170
+ }
1171
+ function plain(parts) {
1172
+ return parts.map((part) => part.code ? `"${part.text}"` : part.text).join("");
1173
+ }
1174
+ function describeLength(result) {
1175
+ const { min, max } = result.length;
1176
+ if (min === 0 && max === 0) {
1177
+ return "Matches the empty string only (zero characters).";
1178
+ }
1179
+ const unit = (n) => n === 1 ? "character" : "characters";
1180
+ if (max === Infinity) {
1181
+ return min === 0 ? "Matches any number of characters, including none." : `Matches ${min} or more characters.`;
1182
+ }
1183
+ if (min === max) {
1184
+ return `Matches exactly ${min} ${unit(min)}.`;
1185
+ }
1186
+ return `Matches ${min} to ${max} characters.`;
1187
+ }
1188
+ function escapePipes(text) {
1189
+ return text.replaceAll("|", "\\|");
1190
+ }
1191
+ function escapeMarkdown(text) {
1192
+ return text.replace(/[\\`*_{}[\]()#+\-.!|<>]/g, "\\$&");
1193
+ }
1194
+ export {
1195
+ ExplanationRenderer,
1196
+ GRAMMARS,
1197
+ RegexFinder,
1198
+ SUPPORTED_LANGUAGE_IDS,
1199
+ describeCount,
1200
+ describeMatches,
1201
+ explainRegex,
1202
+ findMatches,
1203
+ findRegexAt,
1204
+ isRegexSource,
1205
+ parseRegexInput,
1206
+ regex101Url
1207
+ };