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.
- package/LICENSE +201 -0
- package/README.md +275 -1
- package/dist/cjs/compiler.d.ts +34 -0
- package/dist/cjs/compiler.js +160 -0
- package/dist/cjs/explainer.d.ts +123 -0
- package/dist/cjs/explainer.js +628 -0
- package/dist/cjs/extlinks.d.ts +21 -0
- package/dist/cjs/extlinks.js +25 -0
- package/dist/cjs/index.d.ts +22 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/locator.d.ts +80 -0
- package/dist/cjs/locator.js +341 -0
- package/dist/cjs/matches.d.ts +133 -0
- package/dist/cjs/matches.js +238 -0
- package/dist/cjs/render.d.ts +120 -0
- package/dist/cjs/render.js +238 -0
- package/dist/cjs/strings.d.ts +39 -0
- package/dist/cjs/strings.js +85 -0
- package/dist/cjs/text.d.ts +49 -0
- package/dist/cjs/text.js +109 -0
- package/dist/esm/index.mjs +1207 -0
- package/package.json +62 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findMatches = findMatches;
|
|
4
|
+
exports.describeMatches = describeMatches;
|
|
5
|
+
/**
|
|
6
|
+
* Finds the matches of a regex in text, using JavaScript's `RegExp`.
|
|
7
|
+
*
|
|
8
|
+
* @param request The regex, the text, the most matches to return, and whether each line is a
|
|
9
|
+
* separate entry.
|
|
10
|
+
*
|
|
11
|
+
* @returns The matches, or an error if the regex does not compile. Empty matches are reported once
|
|
12
|
+
* at each position, because `matchAll` steps past them.
|
|
13
|
+
*/
|
|
14
|
+
function findMatches(request) {
|
|
15
|
+
let regex;
|
|
16
|
+
try {
|
|
17
|
+
const flags = request.flags.includes("g") ? request.flags : `${request.flags}g`;
|
|
18
|
+
regex = new RegExp(request.pattern, flags);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return { matches: [], truncated: false, error: error.message };
|
|
22
|
+
}
|
|
23
|
+
const lineStarts = findLineStarts(request.text);
|
|
24
|
+
if (!request.entries) {
|
|
25
|
+
// One match past the limit tells whether there are more, without running the regex further.
|
|
26
|
+
const found = [...take(request.text.matchAll(regex), request.limit + 1)];
|
|
27
|
+
return {
|
|
28
|
+
matches: found.slice(0, request.limit).map((match) => toFoundMatch(match, 0, lineStarts)),
|
|
29
|
+
truncated: found.length > request.limit,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const entries = splitEntries(request.text, lineStarts);
|
|
33
|
+
const found = [...take(matchEntries(entries, regex), request.limit + 1)];
|
|
34
|
+
const kept = found.slice(0, request.limit);
|
|
35
|
+
const truncated = found.length > request.limit;
|
|
36
|
+
const lastLine = truncated ? (kept[kept.length - 1]?.entry.line ?? 0) : Infinity;
|
|
37
|
+
return {
|
|
38
|
+
matches: kept.map(({ entry, match }) => toFoundMatch(match, entry.start, lineStarts)),
|
|
39
|
+
truncated,
|
|
40
|
+
entries: entries.filter((entry) => entry.line <= lastLine),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Splits text into entries, one per line.
|
|
45
|
+
*
|
|
46
|
+
* @param text The text.
|
|
47
|
+
* @param lineStarts Offsets from {@link findLineStarts}.
|
|
48
|
+
*
|
|
49
|
+
* @returns The lines without their line breaks; none for empty text, and no empty entry after a
|
|
50
|
+
* final line break.
|
|
51
|
+
*/
|
|
52
|
+
function splitEntries(text, lineStarts) {
|
|
53
|
+
const lines = lineStarts.map((start, i) => {
|
|
54
|
+
const content = text
|
|
55
|
+
.slice(start, lineStarts[i + 1] ?? text.length)
|
|
56
|
+
.replace(/(?:\r\n|\r|\n)$/, "");
|
|
57
|
+
return { line: i + 1, start, end: start + content.length, text: content };
|
|
58
|
+
});
|
|
59
|
+
return text === "" || /(?:\r\n|\r|\n)$/.test(text) ? lines.slice(0, -1) : lines;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs a regex over each entry on its own.
|
|
63
|
+
*
|
|
64
|
+
* @param entries The entries.
|
|
65
|
+
* @param regex The regex, with the `g` flag.
|
|
66
|
+
*
|
|
67
|
+
* @returns Each match with the entry it is in, lazily, so that callers can stop early.
|
|
68
|
+
*/
|
|
69
|
+
function* matchEntries(entries, regex) {
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
for (const match of entry.text.matchAll(regex)) {
|
|
72
|
+
yield { entry, match };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Converts a `matchAll` result into a {@link FoundMatch}.
|
|
78
|
+
*
|
|
79
|
+
* @param match The result.
|
|
80
|
+
* @param offset Where the searched string starts in the whole text: 0, or an entry's start.
|
|
81
|
+
* @param lineStarts Offsets from {@link findLineStarts} for the whole text.
|
|
82
|
+
*
|
|
83
|
+
* @returns The match, with offsets and lines in the whole text.
|
|
84
|
+
*/
|
|
85
|
+
function toFoundMatch(match, offset, lineStarts) {
|
|
86
|
+
const start = offset + match.index;
|
|
87
|
+
const end = start + match[0].length;
|
|
88
|
+
return {
|
|
89
|
+
start,
|
|
90
|
+
end,
|
|
91
|
+
text: match[0],
|
|
92
|
+
startLine: lineAt(lineStarts, start),
|
|
93
|
+
endLine: lineAt(lineStarts, Math.max(start, end - 1)),
|
|
94
|
+
groups: match.slice(1),
|
|
95
|
+
named: { ...match.groups },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Yields the first items of an iterable, without reading past them.
|
|
100
|
+
*
|
|
101
|
+
* @param items The items.
|
|
102
|
+
* @param count How many to yield.
|
|
103
|
+
*
|
|
104
|
+
* @returns The first `count` items, or all of them if there are fewer.
|
|
105
|
+
*/
|
|
106
|
+
function* take(items, count) {
|
|
107
|
+
if (count <= 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let taken = 0;
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
yield item;
|
|
113
|
+
taken += 1;
|
|
114
|
+
if (taken === count) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Words the outcome of a match request for display.
|
|
121
|
+
*
|
|
122
|
+
* @param response The outcome of {@link findMatches}, or one with `timedOut` or `error` set by the
|
|
123
|
+
* caller.
|
|
124
|
+
*
|
|
125
|
+
* @returns The summary sentence, whether it reports a problem, each match with its lines and groups
|
|
126
|
+
* labeled, and, for entries, each entry with its matches.
|
|
127
|
+
*/
|
|
128
|
+
function describeMatches(response) {
|
|
129
|
+
const matches = response.matches.map(describeMatch);
|
|
130
|
+
const entries = response.entries?.map((entry) => {
|
|
131
|
+
const inEntry = matches.filter((match) => match.start >= entry.start && match.start <= entry.end);
|
|
132
|
+
return {
|
|
133
|
+
line: `L${entry.line}`,
|
|
134
|
+
start: entry.start,
|
|
135
|
+
end: entry.end,
|
|
136
|
+
text: entry.text === "" ? "(empty)" : entry.text,
|
|
137
|
+
matched: inEntry.length > 0,
|
|
138
|
+
matches: inEntry,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const problem = !!(response.error || response.timedOut);
|
|
142
|
+
const shown = response.truncated ? ` (showing the first ${matches.length} matches)` : "";
|
|
143
|
+
const summary = response.error
|
|
144
|
+
? `The regex cannot run: ${response.error}`
|
|
145
|
+
: response.timedOut
|
|
146
|
+
? "Matching timed out: the pattern backtracks too much on this text."
|
|
147
|
+
: entries
|
|
148
|
+
? `${describeEntryCount(entries)}${shown}.`
|
|
149
|
+
: `${describeCount(matches.length, "match", "matches")}${response.truncated ? ` (showing the first ${matches.length})` : ""}.`;
|
|
150
|
+
return entries ? { summary, problem, matches, entries } : { summary, problem, matches };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Words how many entries matched.
|
|
154
|
+
*
|
|
155
|
+
* @param entries The entries.
|
|
156
|
+
*
|
|
157
|
+
* @returns A phrase such as "1 of 3 entries matches" or "No entries".
|
|
158
|
+
*/
|
|
159
|
+
function describeEntryCount(entries) {
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
return "No entries";
|
|
162
|
+
}
|
|
163
|
+
const matched = entries.filter((entry) => entry.matched).length;
|
|
164
|
+
const noun = entries.length === 1 ? "entry" : "entries";
|
|
165
|
+
return `${matched} of ${entries.length} ${noun} ${matched === 1 ? "matches" : "match"}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Words a count of things.
|
|
169
|
+
*
|
|
170
|
+
* @param count The count.
|
|
171
|
+
* @param one The noun for one.
|
|
172
|
+
* @param many The noun for several.
|
|
173
|
+
*
|
|
174
|
+
* @returns A phrase such as "No matches", "1 match", or "2 matches".
|
|
175
|
+
*/
|
|
176
|
+
function describeCount(count, one, many) {
|
|
177
|
+
return `${count === 0 ? "No" : count} ${count === 1 ? one : many}`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Words one match for display.
|
|
181
|
+
*
|
|
182
|
+
* @param match The match.
|
|
183
|
+
*
|
|
184
|
+
* @returns The match with its lines and groups labeled.
|
|
185
|
+
*/
|
|
186
|
+
function describeMatch(match) {
|
|
187
|
+
const groups = [
|
|
188
|
+
...match.groups.map((value, i) => [`$${i + 1}`, value]),
|
|
189
|
+
...Object.entries(match.named),
|
|
190
|
+
];
|
|
191
|
+
return {
|
|
192
|
+
start: match.start,
|
|
193
|
+
end: match.end,
|
|
194
|
+
lines: match.startLine === match.endLine
|
|
195
|
+
? `L${match.startLine}`
|
|
196
|
+
: `L${match.startLine}–${match.endLine}`,
|
|
197
|
+
text: match.text === "" ? "(empty)" : match.text,
|
|
198
|
+
groups: groups.map(([name, value]) => ({
|
|
199
|
+
name,
|
|
200
|
+
value: value ?? "(no match)",
|
|
201
|
+
matched: value !== undefined,
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Finds the offset where each line starts.
|
|
207
|
+
*
|
|
208
|
+
* @param text The text.
|
|
209
|
+
*
|
|
210
|
+
* @returns The start offsets, beginning with 0.
|
|
211
|
+
*/
|
|
212
|
+
function findLineStarts(text) {
|
|
213
|
+
const lineBreaks = [...text.matchAll(/\r\n|\r|\n/g)];
|
|
214
|
+
return [0, ...lineBreaks.map((lineBreak) => lineBreak.index + lineBreak[0].length)];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Finds the line containing an offset.
|
|
218
|
+
*
|
|
219
|
+
* @param lineStarts Offsets from {@link findLineStarts}.
|
|
220
|
+
* @param offset The offset.
|
|
221
|
+
*
|
|
222
|
+
* @returns The 1-based line number.
|
|
223
|
+
*/
|
|
224
|
+
function lineAt(lineStarts, offset) {
|
|
225
|
+
let low = 0;
|
|
226
|
+
let high = lineStarts.length - 1;
|
|
227
|
+
while (low < high) {
|
|
228
|
+
const middle = Math.ceil((low + high) / 2);
|
|
229
|
+
if (lineStarts[middle] <= offset) {
|
|
230
|
+
low = middle;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
high = middle - 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return low + 1;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=matches.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ExplainResult } from "./explainer";
|
|
2
|
+
import { TextPart } from "./text";
|
|
3
|
+
/** Options for the {@link ExplanationRenderer} output methods. */
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
/** Start with a heading that shows the whole regex. Defaults to `false`. */
|
|
6
|
+
readonly title?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** One step of an explanation, flattened out of the node tree. */
|
|
9
|
+
export interface ExplanationRow {
|
|
10
|
+
/** The pattern source the step covers. */
|
|
11
|
+
readonly raw: string;
|
|
12
|
+
/** The step's description, which may contain Markdown code spans. */
|
|
13
|
+
readonly description: string;
|
|
14
|
+
/** How deeply the step is nested; top-level steps are 0. */
|
|
15
|
+
readonly depth: number;
|
|
16
|
+
/** The description split into plain text and code, for formats other than Markdown. */
|
|
17
|
+
readonly parts: readonly TextPart[];
|
|
18
|
+
}
|
|
19
|
+
/** A backtracking warning, ready to display. */
|
|
20
|
+
export interface WarningModel {
|
|
21
|
+
/** What the problem is, which may contain Markdown code spans. */
|
|
22
|
+
readonly description: string;
|
|
23
|
+
/** The description split into plain text and code. */
|
|
24
|
+
readonly parts: readonly TextPart[];
|
|
25
|
+
/** An equivalent pattern that does not backtrack, as `/pattern/flags`, if one exists. */
|
|
26
|
+
readonly fix?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A flag's explanation, ready to display. */
|
|
29
|
+
export interface FlagModel {
|
|
30
|
+
/** The flag letter. */
|
|
31
|
+
readonly flag: string;
|
|
32
|
+
/** The flag's name, such as `ignoreCase`. */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** What the flag does, which may contain Markdown code spans. */
|
|
35
|
+
readonly description: string;
|
|
36
|
+
/** The description split into plain text and code. */
|
|
37
|
+
readonly parts: readonly TextPart[];
|
|
38
|
+
}
|
|
39
|
+
/** A syntax error, ready to display. */
|
|
40
|
+
export interface ErrorModel {
|
|
41
|
+
/** The parser's message. */
|
|
42
|
+
readonly message: string;
|
|
43
|
+
/** The pattern. */
|
|
44
|
+
readonly pattern: string;
|
|
45
|
+
/** Offset of the error in the pattern, if known. */
|
|
46
|
+
readonly index?: number;
|
|
47
|
+
/** A line of spaces and a `^` that, printed under the pattern, points at the error. */
|
|
48
|
+
readonly caret?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Everything an explanation view shows, in display order, with descriptions already split so that a
|
|
52
|
+
* renderer only maps it to markup.
|
|
53
|
+
*/
|
|
54
|
+
export interface ExplanationModel {
|
|
55
|
+
/** The regex as `/pattern/flags`. */
|
|
56
|
+
readonly literal: string;
|
|
57
|
+
/** The syntax error, for an invalid pattern; the other lists are then empty. */
|
|
58
|
+
readonly error?: ErrorModel;
|
|
59
|
+
/** Backtracking warnings. */
|
|
60
|
+
readonly warnings: readonly WarningModel[];
|
|
61
|
+
/** The steps, flattened with their depths. */
|
|
62
|
+
readonly rows: readonly ExplanationRow[];
|
|
63
|
+
/** How long a match can be, such as "Matches 3 to 7 characters.", for a valid pattern. */
|
|
64
|
+
readonly length?: string;
|
|
65
|
+
/** The flags' explanations. */
|
|
66
|
+
readonly flags: readonly FlagModel[];
|
|
67
|
+
/** Whether `^` and `$` match at line breaks anywhere in the pattern; false for an error. */
|
|
68
|
+
readonly multiline: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Renders an `explainRegex` result in several output formats. */
|
|
71
|
+
export declare class ExplanationRenderer {
|
|
72
|
+
private readonly result;
|
|
73
|
+
/**
|
|
74
|
+
* Constructs a new ExplanationRenderer.
|
|
75
|
+
*
|
|
76
|
+
* @param result The result of `explainRegex`.
|
|
77
|
+
*/
|
|
78
|
+
constructor(result: ExplainResult);
|
|
79
|
+
/**
|
|
80
|
+
* Collects everything an explanation view shows. Every output format renders from this model.
|
|
81
|
+
*
|
|
82
|
+
* @returns The model.
|
|
83
|
+
*/
|
|
84
|
+
model(): ExplanationModel;
|
|
85
|
+
/**
|
|
86
|
+
* Renders the explanation as a nested Markdown list, suitable for hovers and documentation.
|
|
87
|
+
*
|
|
88
|
+
* @param options Rendering options.
|
|
89
|
+
*
|
|
90
|
+
* @returns The Markdown.
|
|
91
|
+
*/
|
|
92
|
+
markdownList(options?: RenderOptions): string;
|
|
93
|
+
/**
|
|
94
|
+
* Renders the explanation as a Markdown table with a Code Point and an Explanation column. Nested
|
|
95
|
+
* steps are indented with non-breaking spaces.
|
|
96
|
+
*
|
|
97
|
+
* @param options Rendering options.
|
|
98
|
+
*
|
|
99
|
+
* @returns The Markdown.
|
|
100
|
+
*/
|
|
101
|
+
markdownTable(options?: RenderOptions): string;
|
|
102
|
+
/**
|
|
103
|
+
* Renders the explanation as indented plain text, for terminals and logs.
|
|
104
|
+
*
|
|
105
|
+
* @param options Rendering options.
|
|
106
|
+
*
|
|
107
|
+
* @returns The text.
|
|
108
|
+
*/
|
|
109
|
+
text(options?: RenderOptions): string;
|
|
110
|
+
/**
|
|
111
|
+
* Renders the parts every Markdown format shares around its steps: the title, the error or the
|
|
112
|
+
* warnings, the match length, and the flags.
|
|
113
|
+
*
|
|
114
|
+
* @param options Rendering options.
|
|
115
|
+
* @param steps Formats the rows as Markdown lines.
|
|
116
|
+
*
|
|
117
|
+
* @returns The Markdown.
|
|
118
|
+
*/
|
|
119
|
+
private markdown;
|
|
120
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExplanationRenderer = void 0;
|
|
4
|
+
const text_1 = require("./text");
|
|
5
|
+
/** Renders an `explainRegex` result in several output formats. */
|
|
6
|
+
class ExplanationRenderer {
|
|
7
|
+
result;
|
|
8
|
+
/**
|
|
9
|
+
* Constructs a new ExplanationRenderer.
|
|
10
|
+
*
|
|
11
|
+
* @param result The result of `explainRegex`.
|
|
12
|
+
*/
|
|
13
|
+
constructor(result) {
|
|
14
|
+
this.result = result;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Collects everything an explanation view shows. Every output format renders from this model.
|
|
18
|
+
*
|
|
19
|
+
* @returns The model.
|
|
20
|
+
*/
|
|
21
|
+
model() {
|
|
22
|
+
const result = this.result;
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
return {
|
|
25
|
+
literal: literal(result),
|
|
26
|
+
error: {
|
|
27
|
+
message: result.message,
|
|
28
|
+
pattern: result.pattern,
|
|
29
|
+
index: result.index,
|
|
30
|
+
caret: result.index === undefined ? undefined : "^".padStart(result.index + 1),
|
|
31
|
+
},
|
|
32
|
+
warnings: [],
|
|
33
|
+
rows: [],
|
|
34
|
+
flags: [],
|
|
35
|
+
multiline: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
literal: literal(result),
|
|
40
|
+
multiline: result.multiline,
|
|
41
|
+
warnings: result.warnings.map((warning) => ({
|
|
42
|
+
...described(warning.description),
|
|
43
|
+
fix: warning.fix && literal(warning.fix),
|
|
44
|
+
})),
|
|
45
|
+
rows: flatten(result.nodes),
|
|
46
|
+
length: describeLength(result),
|
|
47
|
+
flags: result.flagExplanations.map((f) => ({
|
|
48
|
+
flag: f.flag,
|
|
49
|
+
name: f.name,
|
|
50
|
+
...described(f.description),
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Renders the explanation as a nested Markdown list, suitable for hovers and documentation.
|
|
56
|
+
*
|
|
57
|
+
* @param options Rendering options.
|
|
58
|
+
*
|
|
59
|
+
* @returns The Markdown.
|
|
60
|
+
*/
|
|
61
|
+
markdownList(options = {}) {
|
|
62
|
+
return this.markdown(options, (rows) => rows.map((row) => `${" ".repeat(row.depth)}- ${(0, text_1.code)(row.raw)} ${row.description}`));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Renders the explanation as a Markdown table with a Code Point and an Explanation column. Nested
|
|
66
|
+
* steps are indented with non-breaking spaces.
|
|
67
|
+
*
|
|
68
|
+
* @param options Rendering options.
|
|
69
|
+
*
|
|
70
|
+
* @returns The Markdown.
|
|
71
|
+
*/
|
|
72
|
+
markdownTable(options = {}) {
|
|
73
|
+
return this.markdown(options, (rows) => [
|
|
74
|
+
"| Code Point | Explanation |",
|
|
75
|
+
"| ---------- | ----------- |",
|
|
76
|
+
...rows.map((row) => `| ${" ".repeat(row.depth)}${escapePipes((0, text_1.code)(row.raw))} | ${escapePipes(row.description)} |`),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Renders the explanation as indented plain text, for terminals and logs.
|
|
81
|
+
*
|
|
82
|
+
* @param options Rendering options.
|
|
83
|
+
*
|
|
84
|
+
* @returns The text.
|
|
85
|
+
*/
|
|
86
|
+
text(options = {}) {
|
|
87
|
+
const model = this.model();
|
|
88
|
+
const title = options.title ? [model.literal, ""] : [];
|
|
89
|
+
if (model.error) {
|
|
90
|
+
const caret = model.error.caret === undefined
|
|
91
|
+
? []
|
|
92
|
+
: [` ${model.error.pattern}`, ` ${model.error.caret}`];
|
|
93
|
+
return [...title, `Invalid regular expression: ${model.error.message}`, ...caret].join("\n");
|
|
94
|
+
}
|
|
95
|
+
const warnings = model.warnings.flatMap((warning) => [
|
|
96
|
+
`Warning: ${plain(warning.parts)}.`,
|
|
97
|
+
...(warning.fix ? [` Equivalent safe pattern: ${warning.fix}`] : []),
|
|
98
|
+
]);
|
|
99
|
+
const width = Math.min(30, Math.max(...model.rows.map((row) => row.depth * 2 + row.raw.length)));
|
|
100
|
+
const steps = model.rows.map((row) => `${`${" ".repeat(row.depth)}${row.raw}`.padEnd(width)} ${plain(row.parts)}`);
|
|
101
|
+
const flags = model.flags.length === 0
|
|
102
|
+
? []
|
|
103
|
+
: ["", "Flags:", ...model.flags.map((f) => ` ${f.flag} ${f.name}: ${plain(f.parts)}`)];
|
|
104
|
+
return [
|
|
105
|
+
...title,
|
|
106
|
+
...warnings,
|
|
107
|
+
...(warnings.length > 0 ? [""] : []),
|
|
108
|
+
...steps,
|
|
109
|
+
"",
|
|
110
|
+
model.length ?? "",
|
|
111
|
+
...flags,
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Renders the parts every Markdown format shares around its steps: the title, the error or the
|
|
116
|
+
* warnings, the match length, and the flags.
|
|
117
|
+
*
|
|
118
|
+
* @param options Rendering options.
|
|
119
|
+
* @param steps Formats the rows as Markdown lines.
|
|
120
|
+
*
|
|
121
|
+
* @returns The Markdown.
|
|
122
|
+
*/
|
|
123
|
+
markdown(options, steps) {
|
|
124
|
+
const model = this.model();
|
|
125
|
+
const title = options.title ? ["# Regex explanation", "", (0, text_1.code)(model.literal), ""] : [];
|
|
126
|
+
if (model.error) {
|
|
127
|
+
const caret = model.error.caret === undefined
|
|
128
|
+
? []
|
|
129
|
+
: ["", "```text", model.error.pattern, model.error.caret, "```"];
|
|
130
|
+
const message = `**Invalid regular expression:** ${escapeMarkdown(model.error.message)}`;
|
|
131
|
+
return [...title, message, ...caret].join("\n");
|
|
132
|
+
}
|
|
133
|
+
const warnings = model.warnings.flatMap((warning) => [
|
|
134
|
+
`> **Warning:** ${warning.description}.`,
|
|
135
|
+
...(warning.fix ? [`> Equivalent safe pattern: ${(0, text_1.code)(warning.fix)}`] : []),
|
|
136
|
+
"",
|
|
137
|
+
]);
|
|
138
|
+
const flags = model.flags.length === 0
|
|
139
|
+
? []
|
|
140
|
+
: [
|
|
141
|
+
"",
|
|
142
|
+
"**Flags**",
|
|
143
|
+
"",
|
|
144
|
+
...model.flags.map((f) => `- ${(0, text_1.code)(f.flag)} *${f.name}*: ${f.description}`),
|
|
145
|
+
];
|
|
146
|
+
return [...title, ...warnings, ...steps(model.rows), "", `*${model.length}*`, ...flags].join("\n");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.ExplanationRenderer = ExplanationRenderer;
|
|
150
|
+
/**
|
|
151
|
+
* Formats a regex as a literal.
|
|
152
|
+
*
|
|
153
|
+
* @param source The regex.
|
|
154
|
+
*
|
|
155
|
+
* @returns The pattern between slashes, followed by the flags, such as `/a+/g`.
|
|
156
|
+
*/
|
|
157
|
+
function literal(source) {
|
|
158
|
+
return `/${source.pattern}/${source.flags}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Pairs a description with its parts.
|
|
162
|
+
*
|
|
163
|
+
* @param description A description that may contain Markdown code spans.
|
|
164
|
+
*
|
|
165
|
+
* @returns The description and its parts from {@link splitCodeSpans}.
|
|
166
|
+
*/
|
|
167
|
+
function described(description) {
|
|
168
|
+
return { description, parts: (0, text_1.splitCodeSpans)(description) };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Flattens an explanation tree into rows, each followed by its descendants.
|
|
172
|
+
*
|
|
173
|
+
* @param nodes The sibling nodes.
|
|
174
|
+
* @param depth The depth of `nodes`, where top-level nodes are 0.
|
|
175
|
+
*
|
|
176
|
+
* @returns One row per node, in display order, with its depth.
|
|
177
|
+
*/
|
|
178
|
+
function flatten(nodes, depth = 0) {
|
|
179
|
+
return nodes.flatMap((node) => [
|
|
180
|
+
{ raw: node.raw, depth, ...described(node.description) },
|
|
181
|
+
...flatten(node.children, depth + 1),
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Joins description parts as plain text, with code in double quotes.
|
|
186
|
+
*
|
|
187
|
+
* @param parts The parts.
|
|
188
|
+
*
|
|
189
|
+
* @returns The text.
|
|
190
|
+
*/
|
|
191
|
+
function plain(parts) {
|
|
192
|
+
return parts.map((part) => (part.code ? `"${part.text}"` : part.text)).join("");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Words how long a match can be.
|
|
196
|
+
*
|
|
197
|
+
* @param result A successful explanation.
|
|
198
|
+
*
|
|
199
|
+
* @returns A sentence such as "Matches 3 to 7 characters."
|
|
200
|
+
*/
|
|
201
|
+
function describeLength(result) {
|
|
202
|
+
const { min, max } = result.length;
|
|
203
|
+
if (min === 0 && max === 0) {
|
|
204
|
+
return "Matches the empty string only (zero characters).";
|
|
205
|
+
}
|
|
206
|
+
const unit = (n) => (n === 1 ? "character" : "characters");
|
|
207
|
+
if (max === Infinity) {
|
|
208
|
+
return min === 0
|
|
209
|
+
? "Matches any number of characters, including none."
|
|
210
|
+
: `Matches ${min} or more characters.`;
|
|
211
|
+
}
|
|
212
|
+
if (min === max) {
|
|
213
|
+
return `Matches exactly ${min} ${unit(min)}.`;
|
|
214
|
+
}
|
|
215
|
+
return `Matches ${min} to ${max} characters.`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Escapes the pipes in a Markdown table cell, which GFM treats as cell boundaries even inside code
|
|
219
|
+
* spans.
|
|
220
|
+
*
|
|
221
|
+
* @param text The cell text.
|
|
222
|
+
*
|
|
223
|
+
* @returns The escaped text.
|
|
224
|
+
*/
|
|
225
|
+
function escapePipes(text) {
|
|
226
|
+
return text.replaceAll("|", "\\|");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Escapes Markdown punctuation in text that did not come from this library.
|
|
230
|
+
*
|
|
231
|
+
* @param text The text.
|
|
232
|
+
*
|
|
233
|
+
* @returns The escaped text.
|
|
234
|
+
*/
|
|
235
|
+
function escapeMarkdown(text) {
|
|
236
|
+
return text.replace(/[\\`*_{}[\]()#+\-.!|<>]/g, "\\$&");
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the source text of string literals into the values they denote, so that a pattern written
|
|
3
|
+
* as `"\\d+"` in source is explained as `\d+`. The tree-sitter grammars mark escapes and
|
|
4
|
+
* interpolations; this module only decodes escapes.
|
|
5
|
+
*/
|
|
6
|
+
import type { RegexSource } from "./explainer";
|
|
7
|
+
/**
|
|
8
|
+
* Decodes one escape sequence from a string literal.
|
|
9
|
+
*
|
|
10
|
+
* Unknown single-character escapes such as `\q` decode to the character, as in JavaScript and Ruby.
|
|
11
|
+
* Grammars for languages that keep the backslash, such as Python and PHP, do not mark those as
|
|
12
|
+
* escapes, so they never reach this function.
|
|
13
|
+
*
|
|
14
|
+
* @param escape The escape's source text, starting with a backslash.
|
|
15
|
+
*
|
|
16
|
+
* @returns The decoded text, or undefined for escapes that cannot be decoded without knowing the
|
|
17
|
+
* language, such as Python's `\N{name}`, or `\a` and `\e`, which are control characters in some
|
|
18
|
+
* languages and plain letters in JavaScript.
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeEscape(escape: string): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Splits a PCRE-style pattern such as PHP's `'/a+/i'` into its pattern and flags.
|
|
23
|
+
*
|
|
24
|
+
* @param value The string's value. Its first character is the delimiter; brackets close with their
|
|
25
|
+
* pair.
|
|
26
|
+
*
|
|
27
|
+
* @returns The parts, or undefined if the value is not delimited.
|
|
28
|
+
*/
|
|
29
|
+
export declare function splitDelimitedPattern(value: string): RegexSource | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Rewrites flags into their JavaScript equivalents.
|
|
32
|
+
*
|
|
33
|
+
* @param flags The flags as written in the source language.
|
|
34
|
+
* @param map Replacement for each flag that differs; an empty string drops a flag that does not
|
|
35
|
+
* change what matches. Flags not in the map are kept, so the explainer reports unknown ones.
|
|
36
|
+
*
|
|
37
|
+
* @returns The JavaScript flags.
|
|
38
|
+
*/
|
|
39
|
+
export declare function translateFlags(flags: string, map: Readonly<Record<string, string>>): string;
|