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,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExplanationRenderer = exports.regex101Url = exports.findMatches = exports.describeMatches = exports.RegexFinder = exports.findRegexAt = exports.SUPPORTED_LANGUAGE_IDS = exports.GRAMMARS = exports.parseRegexInput = exports.isRegexSource = exports.explainRegex = exports.describeCount = void 0;
|
|
4
|
+
var explainer_1 = require("./explainer");
|
|
5
|
+
Object.defineProperty(exports, "describeCount", { enumerable: true, get: function () { return explainer_1.describeCount; } });
|
|
6
|
+
Object.defineProperty(exports, "explainRegex", { enumerable: true, get: function () { return explainer_1.explainRegex; } });
|
|
7
|
+
Object.defineProperty(exports, "isRegexSource", { enumerable: true, get: function () { return explainer_1.isRegexSource; } });
|
|
8
|
+
Object.defineProperty(exports, "parseRegexInput", { enumerable: true, get: function () { return explainer_1.parseRegexInput; } });
|
|
9
|
+
var compiler_1 = require("./compiler");
|
|
10
|
+
Object.defineProperty(exports, "GRAMMARS", { enumerable: true, get: function () { return compiler_1.GRAMMARS; } });
|
|
11
|
+
Object.defineProperty(exports, "SUPPORTED_LANGUAGE_IDS", { enumerable: true, get: function () { return compiler_1.SUPPORTED_LANGUAGE_IDS; } });
|
|
12
|
+
var locator_1 = require("./locator");
|
|
13
|
+
Object.defineProperty(exports, "findRegexAt", { enumerable: true, get: function () { return locator_1.findRegexAt; } });
|
|
14
|
+
Object.defineProperty(exports, "RegexFinder", { enumerable: true, get: function () { return locator_1.RegexFinder; } });
|
|
15
|
+
var matches_1 = require("./matches");
|
|
16
|
+
Object.defineProperty(exports, "describeMatches", { enumerable: true, get: function () { return matches_1.describeMatches; } });
|
|
17
|
+
Object.defineProperty(exports, "findMatches", { enumerable: true, get: function () { return matches_1.findMatches; } });
|
|
18
|
+
var extlinks_1 = require("./extlinks");
|
|
19
|
+
Object.defineProperty(exports, "regex101Url", { enumerable: true, get: function () { return extlinks_1.regex101Url; } });
|
|
20
|
+
var render_1 = require("./render");
|
|
21
|
+
Object.defineProperty(exports, "ExplanationRenderer", { enumerable: true, get: function () { return render_1.ExplanationRenderer; } });
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RegexSource } from "./explainer";
|
|
2
|
+
/**
|
|
3
|
+
* A regex found in source text. The pattern has the source language's string escapes decoded, and
|
|
4
|
+
* the flags are translated to JavaScript.
|
|
5
|
+
*/
|
|
6
|
+
export interface LocatedRegex extends RegexSource {
|
|
7
|
+
/** Offset where the regex starts, inclusive. */
|
|
8
|
+
readonly start: number;
|
|
9
|
+
/** Offset where the regex ends, exclusive. */
|
|
10
|
+
readonly end: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Sources longer than this, in UTF-16 code units, are not parsed. Regexes in generated or minified
|
|
14
|
+
* files are rarely worth explaining, and parsing them on every change would be slow.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_SCAN_LENGTH = 1000000;
|
|
17
|
+
/** Finds regexes in documents of the languages in {@link LANGUAGES}. */
|
|
18
|
+
export declare class RegexFinder {
|
|
19
|
+
private readonly wasmDir;
|
|
20
|
+
private readonly grammars;
|
|
21
|
+
private readonly languages;
|
|
22
|
+
/**
|
|
23
|
+
* Constructs a new RegexFinder.
|
|
24
|
+
*
|
|
25
|
+
* @param wasmDir Directory or URL holding `tree-sitter.wasm` and the `tree-sitter-<grammar>.wasm`
|
|
26
|
+
* files, such as the `wasm` directory of `@vscode/tree-sitter-wasm`.
|
|
27
|
+
*/
|
|
28
|
+
constructor(wasmDir: string);
|
|
29
|
+
/**
|
|
30
|
+
* Checks whether regexes can be found in a language.
|
|
31
|
+
*
|
|
32
|
+
* @param languageId A VS Code language ID.
|
|
33
|
+
*
|
|
34
|
+
* @returns Whether the language has a {@link LanguageSpec}.
|
|
35
|
+
*/
|
|
36
|
+
supports(languageId: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Lists the regexes in source text, in order.
|
|
39
|
+
*
|
|
40
|
+
* @param source The source text.
|
|
41
|
+
* @param languageId The VS Code language ID of the source.
|
|
42
|
+
*
|
|
43
|
+
* @returns The regexes found; none for unsupported languages or for sources longer than
|
|
44
|
+
* {@link MAX_SCAN_LENGTH}. Patterns built at run time, such as strings with interpolations, are
|
|
45
|
+
* skipped.
|
|
46
|
+
*/
|
|
47
|
+
find(source: string, languageId: string): Promise<LocatedRegex[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a parser and compiles the query for a language, once per language.
|
|
50
|
+
*
|
|
51
|
+
* @param languageId The VS Code language ID.
|
|
52
|
+
* @param spec The language's entry in {@link LANGUAGES}.
|
|
53
|
+
*
|
|
54
|
+
* @returns The parser and query.
|
|
55
|
+
*/
|
|
56
|
+
private language;
|
|
57
|
+
/**
|
|
58
|
+
* Loads a grammar from the WASM directory, once per grammar.
|
|
59
|
+
*
|
|
60
|
+
* @param grammar The grammar name.
|
|
61
|
+
*
|
|
62
|
+
* @returns The grammar.
|
|
63
|
+
*/
|
|
64
|
+
private grammar;
|
|
65
|
+
/**
|
|
66
|
+
* Loads the tree-sitter runtime, once per process.
|
|
67
|
+
*
|
|
68
|
+
* @returns The runtime's exports.
|
|
69
|
+
*/
|
|
70
|
+
private runtime;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Finds the regex that contains an offset.
|
|
74
|
+
*
|
|
75
|
+
* @param regexes Regexes from {@link RegexFinder.find}, in order.
|
|
76
|
+
* @param offset The offset to look up.
|
|
77
|
+
*
|
|
78
|
+
* @returns The regex, or undefined if the offset is not inside one.
|
|
79
|
+
*/
|
|
80
|
+
export declare function findRegexAt(regexes: readonly LocatedRegex[], offset: number): LocatedRegex | undefined;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RegexFinder = exports.MAX_SCAN_LENGTH = void 0;
|
|
4
|
+
exports.findRegexAt = findRegexAt;
|
|
5
|
+
const compiler_1 = require("./compiler");
|
|
6
|
+
const strings_1 = require("./strings");
|
|
7
|
+
/**
|
|
8
|
+
* Sources longer than this, in UTF-16 code units, are not parsed. Regexes in generated or minified
|
|
9
|
+
* files are rarely worth explaining, and parsing them on every change would be slow.
|
|
10
|
+
*/
|
|
11
|
+
exports.MAX_SCAN_LENGTH = 1_000_000;
|
|
12
|
+
/** Named children of string literals that hold their text rather than delimiters or code. */
|
|
13
|
+
const CONTENT_TYPE = /(?:content|fragment)$/;
|
|
14
|
+
/** Named children of string literals that delimit them. */
|
|
15
|
+
const DELIMITER_TYPE = /(?:_start|_end)$/;
|
|
16
|
+
/** `<script>` blocks of single-file components, with the code in group 1. */
|
|
17
|
+
const SCRIPT_BLOCK = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
18
|
+
/**
|
|
19
|
+
* Loads and initializes the tree-sitter runtime once per process, from the first finder's
|
|
20
|
+
* directory.
|
|
21
|
+
*/
|
|
22
|
+
let runtime;
|
|
23
|
+
/**
|
|
24
|
+
* Loads the tree-sitter runtime and initializes its WebAssembly module.
|
|
25
|
+
*
|
|
26
|
+
* @param wasmDir Directory holding `tree-sitter.wasm`.
|
|
27
|
+
*
|
|
28
|
+
* @returns The runtime's exports.
|
|
29
|
+
*/
|
|
30
|
+
async function loadRuntime(wasmDir) {
|
|
31
|
+
const imported = await import("@vscode/tree-sitter-wasm");
|
|
32
|
+
// The package is CommonJS: ES module loaders put its exports on `default`.
|
|
33
|
+
const treeSitter = imported.default ?? imported;
|
|
34
|
+
await treeSitter.Parser.init({ locateFile: (file) => joinPath(wasmDir, file) });
|
|
35
|
+
return treeSitter;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Joins a directory and a file name with `/`, which works for file paths on every platform and for
|
|
39
|
+
* URLs, so that the finder runs under bundlers without Node's `path` module.
|
|
40
|
+
*
|
|
41
|
+
* @param dir The directory or URL.
|
|
42
|
+
* @param file The file name.
|
|
43
|
+
*
|
|
44
|
+
* @returns The joined path.
|
|
45
|
+
*/
|
|
46
|
+
function joinPath(dir, file) {
|
|
47
|
+
return `${dir.replace(/[\\/]+$/, "")}/${file}`;
|
|
48
|
+
}
|
|
49
|
+
/** Finds regexes in documents of the languages in {@link LANGUAGES}. */
|
|
50
|
+
class RegexFinder {
|
|
51
|
+
wasmDir;
|
|
52
|
+
grammars = new Map();
|
|
53
|
+
languages = new Map();
|
|
54
|
+
/**
|
|
55
|
+
* Constructs a new RegexFinder.
|
|
56
|
+
*
|
|
57
|
+
* @param wasmDir Directory or URL holding `tree-sitter.wasm` and the `tree-sitter-<grammar>.wasm`
|
|
58
|
+
* files, such as the `wasm` directory of `@vscode/tree-sitter-wasm`.
|
|
59
|
+
*/
|
|
60
|
+
constructor(wasmDir) {
|
|
61
|
+
this.wasmDir = wasmDir;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks whether regexes can be found in a language.
|
|
65
|
+
*
|
|
66
|
+
* @param languageId A VS Code language ID.
|
|
67
|
+
*
|
|
68
|
+
* @returns Whether the language has a {@link LanguageSpec}.
|
|
69
|
+
*/
|
|
70
|
+
supports(languageId) {
|
|
71
|
+
return Object.hasOwn(compiler_1.LANGUAGES, languageId);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lists the regexes in source text, in order.
|
|
75
|
+
*
|
|
76
|
+
* @param source The source text.
|
|
77
|
+
* @param languageId The VS Code language ID of the source.
|
|
78
|
+
*
|
|
79
|
+
* @returns The regexes found; none for unsupported languages or for sources longer than
|
|
80
|
+
* {@link MAX_SCAN_LENGTH}. Patterns built at run time, such as strings with interpolations, are
|
|
81
|
+
* skipped.
|
|
82
|
+
*/
|
|
83
|
+
async find(source, languageId) {
|
|
84
|
+
if (source.length > exports.MAX_SCAN_LENGTH || !this.supports(languageId)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const spec = compiler_1.LANGUAGES[languageId];
|
|
88
|
+
const { parser, query } = await this.language(languageId, spec);
|
|
89
|
+
const includedRanges = spec.scriptBlocks ? scriptRanges(source) : undefined;
|
|
90
|
+
if (includedRanges?.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const tree = parser.parse(source, null, { includedRanges });
|
|
94
|
+
if (!tree) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const regexes = query
|
|
99
|
+
.matches(tree.rootNode)
|
|
100
|
+
.flatMap((match) => readMatch(match.captures, source, spec) ?? []);
|
|
101
|
+
// Patterns can match the same regex more than once; the first match at each offset wins, so
|
|
102
|
+
// the map is built from the end, where later entries replace earlier ones.
|
|
103
|
+
const byStart = new Map([...regexes].reverse().map((regex) => [regex.start, regex]));
|
|
104
|
+
return [...byStart.values()].sort((a, b) => a.start - b.start);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
tree.delete();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Creates a parser and compiles the query for a language, once per language.
|
|
112
|
+
*
|
|
113
|
+
* @param languageId The VS Code language ID.
|
|
114
|
+
* @param spec The language's entry in {@link LANGUAGES}.
|
|
115
|
+
*
|
|
116
|
+
* @returns The parser and query.
|
|
117
|
+
*/
|
|
118
|
+
language(languageId, spec) {
|
|
119
|
+
let loaded = this.languages.get(languageId);
|
|
120
|
+
if (!loaded) {
|
|
121
|
+
loaded = Promise.all([this.runtime(), this.grammar(spec.grammar)]).then(([treeSitter, language]) => {
|
|
122
|
+
const parser = new treeSitter.Parser();
|
|
123
|
+
parser.setLanguage(language);
|
|
124
|
+
return { parser, query: new treeSitter.Query(language, spec.query) };
|
|
125
|
+
});
|
|
126
|
+
// A cache: each language's parser and query are built once and shared by later calls.
|
|
127
|
+
// eslint-disable-next-line functional/immutable-data
|
|
128
|
+
this.languages.set(languageId, loaded);
|
|
129
|
+
}
|
|
130
|
+
return loaded;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Loads a grammar from the WASM directory, once per grammar.
|
|
134
|
+
*
|
|
135
|
+
* @param grammar The grammar name.
|
|
136
|
+
*
|
|
137
|
+
* @returns The grammar.
|
|
138
|
+
*/
|
|
139
|
+
grammar(grammar) {
|
|
140
|
+
let loaded = this.grammars.get(grammar);
|
|
141
|
+
if (!loaded) {
|
|
142
|
+
loaded = this.runtime().then((treeSitter) => treeSitter.Language.load(joinPath(this.wasmDir, `tree-sitter-${grammar}.wasm`)));
|
|
143
|
+
// A cache: each grammar's WASM file is loaded once and shared by later calls.
|
|
144
|
+
// eslint-disable-next-line functional/immutable-data
|
|
145
|
+
this.grammars.set(grammar, loaded);
|
|
146
|
+
}
|
|
147
|
+
return loaded;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Loads the tree-sitter runtime, once per process.
|
|
151
|
+
*
|
|
152
|
+
* @returns The runtime's exports.
|
|
153
|
+
*/
|
|
154
|
+
runtime() {
|
|
155
|
+
runtime ??= loadRuntime(this.wasmDir);
|
|
156
|
+
return runtime;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.RegexFinder = RegexFinder;
|
|
160
|
+
/**
|
|
161
|
+
* Finds the regex that contains an offset.
|
|
162
|
+
*
|
|
163
|
+
* @param regexes Regexes from {@link RegexFinder.find}, in order.
|
|
164
|
+
* @param offset The offset to look up.
|
|
165
|
+
*
|
|
166
|
+
* @returns The regex, or undefined if the offset is not inside one.
|
|
167
|
+
*/
|
|
168
|
+
function findRegexAt(regexes, offset) {
|
|
169
|
+
return regexes.find((r) => offset >= r.start && offset < r.end);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Reads the pattern and flags from one query match.
|
|
173
|
+
*
|
|
174
|
+
* @param captures The match's captures.
|
|
175
|
+
* @param source The parsed source text.
|
|
176
|
+
* @param spec The language.
|
|
177
|
+
*
|
|
178
|
+
* @returns The regex, or undefined if its value is not known until run time.
|
|
179
|
+
*/
|
|
180
|
+
function readMatch(captures, source, spec) {
|
|
181
|
+
const node = (name) => captures.find((c) => c.name === name)?.node;
|
|
182
|
+
const literal = node("literal");
|
|
183
|
+
const delimited = node("pattern.delimited");
|
|
184
|
+
const patternString = node("pattern.string");
|
|
185
|
+
const flagsString = node("flags.string");
|
|
186
|
+
const patternNode = node("pattern");
|
|
187
|
+
let parts;
|
|
188
|
+
if (literal) {
|
|
189
|
+
parts = literalParts(literal, source);
|
|
190
|
+
}
|
|
191
|
+
else if (delimited) {
|
|
192
|
+
const value = stringValue(delimited, source);
|
|
193
|
+
parts = value === undefined ? undefined : (0, strings_1.splitDelimitedPattern)(value);
|
|
194
|
+
}
|
|
195
|
+
else if (patternString) {
|
|
196
|
+
const pattern = stringValue(patternString, source);
|
|
197
|
+
const flags = flagsString ? stringValue(flagsString, source) : "";
|
|
198
|
+
parts = pattern === undefined || flags === undefined ? undefined : { pattern, flags };
|
|
199
|
+
}
|
|
200
|
+
else if (patternNode) {
|
|
201
|
+
parts = { pattern: patternNode.text, flags: node("flags")?.text ?? "" };
|
|
202
|
+
}
|
|
203
|
+
const range = node("regex") ?? literal ?? delimited ?? patternString ?? patternNode;
|
|
204
|
+
if (!parts || !range) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
pattern: parts.pattern,
|
|
209
|
+
flags: (0, strings_1.translateFlags)(parts.flags, spec.flags ?? {}),
|
|
210
|
+
start: range.startIndex,
|
|
211
|
+
end: range.endIndex,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Splits a regex literal into the source text between its delimiters and the flags after them.
|
|
216
|
+
*
|
|
217
|
+
* @param node A `@literal` capture.
|
|
218
|
+
* @param source The parsed source text.
|
|
219
|
+
*
|
|
220
|
+
* @returns The parts, or undefined if the literal interpolates code.
|
|
221
|
+
*/
|
|
222
|
+
function literalParts(node, source) {
|
|
223
|
+
const open = node.firstChild;
|
|
224
|
+
const close = node.lastChild;
|
|
225
|
+
if (!open || !close || open.startIndex === close.startIndex || node.namedChildren.some(isCode)) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
pattern: source.slice(open.endIndex, close.startIndex),
|
|
230
|
+
flags: close.text.slice(1),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Computes the value of a string literal, decoding its escapes.
|
|
235
|
+
*
|
|
236
|
+
* @param node A string literal node.
|
|
237
|
+
* @param source The parsed source text.
|
|
238
|
+
*
|
|
239
|
+
* @returns The value, or undefined if the string interpolates code or has an escape that cannot be
|
|
240
|
+
* decoded.
|
|
241
|
+
*/
|
|
242
|
+
function stringValue(node, source) {
|
|
243
|
+
if (node.childCount === 0) {
|
|
244
|
+
return verbatimValue(node.text);
|
|
245
|
+
}
|
|
246
|
+
let value = "";
|
|
247
|
+
for (const child of node.children) {
|
|
248
|
+
if (!child || !child.isNamed || DELIMITER_TYPE.test(child.type)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const piece = child.type === "escape_sequence"
|
|
252
|
+
? (0, strings_1.decodeEscape)(child.text)
|
|
253
|
+
: CONTENT_TYPE.test(child.type)
|
|
254
|
+
? contentValue(child, source)
|
|
255
|
+
: undefined;
|
|
256
|
+
if (piece === undefined) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
value += piece;
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Computes the value of a string content node, some of which, as in Python, contain the escapes.
|
|
265
|
+
*
|
|
266
|
+
* @param node The content node.
|
|
267
|
+
* @param source The parsed source text.
|
|
268
|
+
*
|
|
269
|
+
* @returns The value, or undefined if an escape cannot be decoded.
|
|
270
|
+
*/
|
|
271
|
+
function contentValue(node, source) {
|
|
272
|
+
let value = "";
|
|
273
|
+
let offset = node.startIndex;
|
|
274
|
+
for (const escape of node.descendantsOfType("escape_sequence")) {
|
|
275
|
+
const decoded = escape ? (0, strings_1.decodeEscape)(escape.text) : undefined;
|
|
276
|
+
if (!escape || decoded === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
value += source.slice(offset, escape.startIndex) + decoded;
|
|
280
|
+
offset = escape.endIndex;
|
|
281
|
+
}
|
|
282
|
+
return value + source.slice(offset, node.endIndex);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Computes the value of a string literal that the grammar does not split into parts, such as C#'s
|
|
286
|
+
* `@"..."`, where a doubled quote stands for one quote.
|
|
287
|
+
*
|
|
288
|
+
* @param text The literal's source text.
|
|
289
|
+
*
|
|
290
|
+
* @returns The value, or undefined if the text is not a quoted string.
|
|
291
|
+
*/
|
|
292
|
+
function verbatimValue(text) {
|
|
293
|
+
const quoted = /^[^"'`]*(["'`])([\s\S]*)\1$/.exec(text);
|
|
294
|
+
return quoted ? quoted[2].replaceAll(quoted[1] + quoted[1], quoted[1]) : undefined;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Checks whether a child of a literal is code, such as an interpolation, rather than text.
|
|
298
|
+
*
|
|
299
|
+
* @param node A named child.
|
|
300
|
+
*
|
|
301
|
+
* @returns Whether the node is neither text, an escape, nor a delimiter.
|
|
302
|
+
*/
|
|
303
|
+
function isCode(node) {
|
|
304
|
+
return (!!node &&
|
|
305
|
+
node.type !== "escape_sequence" &&
|
|
306
|
+
!CONTENT_TYPE.test(node.type) &&
|
|
307
|
+
!DELIMITER_TYPE.test(node.type));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Lists the `<script>` blocks of a single-file component as parser ranges.
|
|
311
|
+
*
|
|
312
|
+
* @param source The component's source text.
|
|
313
|
+
*
|
|
314
|
+
* @returns One range per block, covering the code between the tags.
|
|
315
|
+
*/
|
|
316
|
+
function scriptRanges(source) {
|
|
317
|
+
return [...source.matchAll(SCRIPT_BLOCK)].map((block) => {
|
|
318
|
+
const startIndex = (block.index ?? 0) + block[0].indexOf(">") + 1;
|
|
319
|
+
const endIndex = startIndex + block[1].length;
|
|
320
|
+
return {
|
|
321
|
+
startIndex,
|
|
322
|
+
endIndex,
|
|
323
|
+
startPosition: pointAt(source, startIndex),
|
|
324
|
+
endPosition: pointAt(source, endIndex),
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Converts an offset to a row and column.
|
|
330
|
+
*
|
|
331
|
+
* @param source The text.
|
|
332
|
+
* @param offset The offset.
|
|
333
|
+
*
|
|
334
|
+
* @returns The zero-based row and column.
|
|
335
|
+
*/
|
|
336
|
+
function pointAt(source, offset) {
|
|
337
|
+
const before = source.slice(0, offset);
|
|
338
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
339
|
+
return { row: before.split("\n").length - 1, column: offset - lineStart };
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=locator.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a regex over text and reports each match with the lines it covers, and words the outcome for
|
|
3
|
+
* display. The text is matched either as one string or, with `entries`, one line at a time.
|
|
4
|
+
*/
|
|
5
|
+
import type { RegexSource } from "./explainer";
|
|
6
|
+
/** Text to match against a regex. `g` is added to the flags if missing, so every match is found. */
|
|
7
|
+
export interface MatchRequest extends RegexSource {
|
|
8
|
+
/** The text to search. */
|
|
9
|
+
readonly text: string;
|
|
10
|
+
/** The most matches to return. */
|
|
11
|
+
readonly limit: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether each line of the text is a separate entry, matched on its own, so that `^` and `$`
|
|
14
|
+
* match at its ends and no match spans lines. A line break at the end of the text does not start
|
|
15
|
+
* another entry.
|
|
16
|
+
*/
|
|
17
|
+
readonly entries?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** One match in the text. */
|
|
20
|
+
export interface FoundMatch {
|
|
21
|
+
/** Offset of the first matched character, in UTF-16 code units. */
|
|
22
|
+
readonly start: number;
|
|
23
|
+
/** Offset just past the last matched character. */
|
|
24
|
+
readonly end: number;
|
|
25
|
+
/** The matched text. */
|
|
26
|
+
readonly text: string;
|
|
27
|
+
/** The 1-based line the match starts on. */
|
|
28
|
+
readonly startLine: number;
|
|
29
|
+
/** The 1-based line the match ends on; equal to `startLine` for single-line matches. */
|
|
30
|
+
readonly endLine: number;
|
|
31
|
+
/** The numbered capture groups' values, starting with group 1; undefined if not matched. */
|
|
32
|
+
readonly groups: readonly (string | undefined)[];
|
|
33
|
+
/** The named capture groups' values, by name. */
|
|
34
|
+
readonly named: Readonly<Record<string, string | undefined>>;
|
|
35
|
+
}
|
|
36
|
+
/** A line of the text matched on its own, in a request with `entries`. */
|
|
37
|
+
export interface MatchEntry {
|
|
38
|
+
/** The 1-based line. */
|
|
39
|
+
readonly line: number;
|
|
40
|
+
/** Offset of the line's first character. */
|
|
41
|
+
readonly start: number;
|
|
42
|
+
/** Offset just past the line's last character, before its line break. */
|
|
43
|
+
readonly end: number;
|
|
44
|
+
/** The line, without its line break. */
|
|
45
|
+
readonly text: string;
|
|
46
|
+
}
|
|
47
|
+
/** The outcome of a {@link MatchRequest}. */
|
|
48
|
+
export interface MatchResponse {
|
|
49
|
+
/** The matches, in order. */
|
|
50
|
+
readonly matches: readonly FoundMatch[];
|
|
51
|
+
/** Whether matching stopped at the limit. */
|
|
52
|
+
readonly truncated: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The entries matched, for a request with `entries`. When matching stopped at the limit, the
|
|
55
|
+
* entries after the one holding the last match are left out.
|
|
56
|
+
*/
|
|
57
|
+
readonly entries?: readonly MatchEntry[];
|
|
58
|
+
/** Why the regex could not run, such as a syntax error. */
|
|
59
|
+
readonly error?: string;
|
|
60
|
+
/** Whether matching was stopped because it took too long. */
|
|
61
|
+
readonly timedOut?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** A capture group of a match, ready to display. */
|
|
64
|
+
export interface MatchGroupModel {
|
|
65
|
+
/** The group's label: `$1`, `$2`, and so on for numbered groups, or the group's name. */
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** The group's text, or "(no match)" when it did not take part in the match. */
|
|
68
|
+
readonly value: string;
|
|
69
|
+
/** Whether the group matched. */
|
|
70
|
+
readonly matched: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** A match, ready to display. */
|
|
73
|
+
export interface MatchModel {
|
|
74
|
+
/** Offset of the first matched character, in UTF-16 code units. */
|
|
75
|
+
readonly start: number;
|
|
76
|
+
/** Offset just past the last matched character. */
|
|
77
|
+
readonly end: number;
|
|
78
|
+
/** The lines the match covers, such as `L3` or `L1–3`. */
|
|
79
|
+
readonly lines: string;
|
|
80
|
+
/** The matched text, or "(empty)" for an empty match. */
|
|
81
|
+
readonly text: string;
|
|
82
|
+
/** The numbered groups, then the named ones. */
|
|
83
|
+
readonly groups: readonly MatchGroupModel[];
|
|
84
|
+
}
|
|
85
|
+
/** An entry and its matches, ready to display. */
|
|
86
|
+
export interface EntryModel {
|
|
87
|
+
/** The entry's line, such as `L3`. */
|
|
88
|
+
readonly line: string;
|
|
89
|
+
/** Offset of the entry's first character. */
|
|
90
|
+
readonly start: number;
|
|
91
|
+
/** Offset just past the entry's last character. */
|
|
92
|
+
readonly end: number;
|
|
93
|
+
/** The entry's text, or "(empty)" for an empty line. */
|
|
94
|
+
readonly text: string;
|
|
95
|
+
/** Whether the regex matched the entry. */
|
|
96
|
+
readonly matched: boolean;
|
|
97
|
+
/** The matches in the entry, in order. */
|
|
98
|
+
readonly matches: readonly MatchModel[];
|
|
99
|
+
}
|
|
100
|
+
/** Everything a view of a {@link MatchResponse} shows. */
|
|
101
|
+
export interface MatchesModel {
|
|
102
|
+
/**
|
|
103
|
+
* One sentence on the outcome, such as "2 matches.", "1 of 3 entries matches.", or why the regex
|
|
104
|
+
* could not run.
|
|
105
|
+
*/
|
|
106
|
+
readonly summary: string;
|
|
107
|
+
/** Whether the regex could not run to completion, so the summary reports a problem. */
|
|
108
|
+
readonly problem: boolean;
|
|
109
|
+
/** The matches, in order. */
|
|
110
|
+
readonly matches: readonly MatchModel[];
|
|
111
|
+
/** Each entry with its matches, when the request had `entries`. */
|
|
112
|
+
readonly entries?: readonly EntryModel[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Finds the matches of a regex in text, using JavaScript's `RegExp`.
|
|
116
|
+
*
|
|
117
|
+
* @param request The regex, the text, the most matches to return, and whether each line is a
|
|
118
|
+
* separate entry.
|
|
119
|
+
*
|
|
120
|
+
* @returns The matches, or an error if the regex does not compile. Empty matches are reported once
|
|
121
|
+
* at each position, because `matchAll` steps past them.
|
|
122
|
+
*/
|
|
123
|
+
export declare function findMatches(request: MatchRequest): MatchResponse;
|
|
124
|
+
/**
|
|
125
|
+
* Words the outcome of a match request for display.
|
|
126
|
+
*
|
|
127
|
+
* @param response The outcome of {@link findMatches}, or one with `timedOut` or `error` set by the
|
|
128
|
+
* caller.
|
|
129
|
+
*
|
|
130
|
+
* @returns The summary sentence, whether it reports a problem, each match with its lines and groups
|
|
131
|
+
* labeled, and, for entries, each entry with its matches.
|
|
132
|
+
*/
|
|
133
|
+
export declare function describeMatches(response: MatchResponse): MatchesModel;
|