poe-code 14.0.74 → 14.0.76
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/dist/index.js +1283 -144
- package/dist/index.js.map +4 -4
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/safe-js/dist/chunks/{chunk-YEZOXLY4.js → chunk-UZYQWFMY.js} +1233 -143
- package/packages/safe-js/dist/chunks/chunk-UZYQWFMY.js.map +7 -0
- package/packages/safe-js/dist/chunks/{chunk-2J6W3YGB.js → chunk-VD2AS4E4.js} +2 -2
- package/packages/safe-js/dist/cli.js +2 -2
- package/packages/safe-js/dist/core.js +1 -1
- package/packages/safe-js/dist/index.js +2 -2
- package/packages/safe-js/dist/index.js.map +1 -1
- package/packages/safe-js/dist/interp/bigint-operators.d.ts +3 -0
- package/packages/safe-js/dist/interp/bigint-operators.js +62 -0
- package/packages/safe-js/dist/interp/boxed.d.ts +2 -2
- package/packages/safe-js/dist/interp/boxed.js +4 -1
- package/packages/safe-js/dist/interp/globals/bigint.d.ts +2 -0
- package/packages/safe-js/dist/interp/globals/bigint.js +85 -0
- package/packages/safe-js/dist/interp/globals/console-json.js +57 -24
- package/packages/safe-js/dist/interp/globals/object-array.js +1 -1
- package/packages/safe-js/dist/interp/globals/regex.js +108 -2
- package/packages/safe-js/dist/interp/globals.d.ts +1 -0
- package/packages/safe-js/dist/interp/globals.js +2 -0
- package/packages/safe-js/dist/interp/host-bridge.js +2 -0
- package/packages/safe-js/dist/interp/interpreter.js +59 -12
- package/packages/safe-js/dist/interp/iteration.js +5 -2
- package/packages/safe-js/dist/interp/methods/number.js +1 -1
- package/packages/safe-js/dist/interp/methods/regex-split.d.ts +3 -0
- package/packages/safe-js/dist/interp/methods/regex-split.js +102 -0
- package/packages/safe-js/dist/interp/methods/regex.d.ts +1 -1
- package/packages/safe-js/dist/interp/methods/regex.js +16 -5
- package/packages/safe-js/dist/interp/methods/regexp-iterator.d.ts +5 -1
- package/packages/safe-js/dist/interp/methods/regexp-iterator.js +59 -7
- package/packages/safe-js/dist/interp/methods/string.js +9 -9
- package/packages/safe-js/dist/interp/object-model.d.ts +1 -0
- package/packages/safe-js/dist/interp/object-model.js +4 -1
- package/packages/safe-js/dist/interp/regex/compile-guard.js +26 -4
- package/packages/safe-js/dist/interp/regex/engine.d.ts +4 -0
- package/packages/safe-js/dist/interp/regex/engine.js +230 -30
- package/packages/safe-js/dist/interp/regex/parse.d.ts +42 -4
- package/packages/safe-js/dist/interp/regex/parse.js +429 -25
- package/packages/safe-js/dist/interp/regexp-iterator.d.ts +4 -2
- package/packages/safe-js/dist/interp/regexp-iterator.js +6 -1
- package/packages/safe-js/dist/interp/string-coercion.d.ts +1 -1
- package/packages/safe-js/dist/interp/string-coercion.js +8 -3
- package/packages/safe-js/dist/interp/values.d.ts +1 -1
- package/packages/safe-js/dist/interp/values.js +10 -3
- package/packages/safe-js/dist/lint/rules/AS-async-not-needed.js +2 -0
- package/packages/safe-js/dist/lint/rules/AS-missing-async.js +2 -0
- package/packages/safe-js/dist/lint/rules/known-globals.d.ts +1 -1
- package/packages/safe-js/dist/lint/rules/known-globals.js +1 -0
- package/packages/safe-js/dist/parse/parser.d.ts +6 -1
- package/packages/safe-js/dist/parse/parser.js +8 -0
- package/packages/safe-js/dist/parse/tokenizer.js +12 -4
- package/packages/safe-js/dist/parse.d.ts +1 -1
- package/packages/safe-js/dist/snapshot/bigint.d.ts +1 -0
- package/packages/safe-js/dist/snapshot/bigint.js +14 -0
- package/packages/safe-js/dist/snapshot/dump-format.js +12 -2
- package/packages/safe-js/dist/snapshot/replay-data.d.ts +6 -0
- package/packages/safe-js/dist/snapshot/replay-data.js +23 -5
- package/packages/safe-js/dist/snapshot/restore.js +10 -2
- package/packages/safe-js/dist/snapshot/serialize.d.ts +4 -1
- package/packages/safe-js/dist/snapshot/serialize.js +6 -1
- package/packages/safe-js/dist/snapshot/validation.js +27 -0
- package/packages/safe-js/dist/chunks/chunk-YEZOXLY4.js.map +0 -7
- /package/packages/safe-js/dist/chunks/{chunk-2J6W3YGB.js.map → chunk-VD2AS4E4.js.map} +0 -0
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { isSandboxRegExpIterator, regexpIteratorState } from "../regexp-iterator.js";
|
|
2
|
-
import { createSandboxClosure } from "../values.js";
|
|
3
|
-
import { executeRegex, toMatchArray } from "./regex.js";
|
|
2
|
+
import { createSandboxClosure, isSandboxRegex } from "../values.js";
|
|
3
|
+
import { executeRegex, regexExec, toMatchArray } from "./regex.js";
|
|
4
|
+
import { getSandboxPropertyDescriptor, hasRegexPropertyOverride } from "../object-model.js";
|
|
5
|
+
import { readPropertyDescriptor } from "../accessors.js";
|
|
6
|
+
import { setSandboxProperty } from "../interpreter.js";
|
|
7
|
+
import { sandboxNumber, sandboxString } from "../string-coercion.js";
|
|
8
|
+
import { normalizeLastIndex } from "../regex/engine.js";
|
|
9
|
+
import { retainValues } from "../resources.js";
|
|
4
10
|
export function nextRegExpIterator(iterator, budget) {
|
|
5
11
|
const state = regexpIteratorState(iterator);
|
|
6
12
|
budget?.visitNode();
|
|
7
13
|
if (state.exhausted)
|
|
8
14
|
return { value: undefined, done: true };
|
|
9
15
|
const matcher = state.matcher;
|
|
16
|
+
if (!isSandboxRegex(matcher))
|
|
17
|
+
throw new TypeError("Custom RegExp iterator execution requires a sandbox context.");
|
|
10
18
|
const input = state.input;
|
|
11
19
|
const match = executeRegex(matcher, input, Number(matcher.lastIndex));
|
|
12
|
-
if (match === null || !matcher.flags.includes("g")) {
|
|
20
|
+
if (match === null || !(state.global ?? matcher.flags.includes("g"))) {
|
|
13
21
|
state.exhausted = true;
|
|
14
22
|
state.matcher = undefined;
|
|
15
23
|
state.input = undefined;
|
|
@@ -18,12 +26,56 @@ export function nextRegExpIterator(iterator, budget) {
|
|
|
18
26
|
return { value: undefined, done: true };
|
|
19
27
|
if (!state.exhausted && match.text.length === 0) {
|
|
20
28
|
const index = Number(matcher.lastIndex);
|
|
21
|
-
const unicode = matcher.flags.includes("u") || matcher.flags.includes("v");
|
|
29
|
+
const unicode = state.unicode ?? (matcher.flags.includes("u") || matcher.flags.includes("v"));
|
|
22
30
|
const codePoint = input.codePointAt(index);
|
|
23
31
|
matcher.lastIndex = index + (unicode && codePoint !== undefined && codePoint > 0xffff ? 2 : 1);
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
return { value: toMatchArray(match, input, budget), done: false };
|
|
34
|
+
}
|
|
35
|
+
export async function nextObservableRegExpIterator(iterator, budget, context) {
|
|
36
|
+
const state = regexpIteratorState(iterator);
|
|
37
|
+
if (state.exhausted)
|
|
38
|
+
return { value: undefined, done: true };
|
|
39
|
+
const matcher = state.matcher;
|
|
40
|
+
if (isSandboxRegex(matcher) && !hasRegexPropertyOverride(matcher, ["exec"], budget) &&
|
|
41
|
+
(matcher.lastIndex === null || typeof matcher.lastIndex !== "object"))
|
|
42
|
+
return nextRegExpIterator(iterator, budget);
|
|
43
|
+
const input = state.input;
|
|
44
|
+
let result;
|
|
45
|
+
let field;
|
|
46
|
+
const release = retainValues(budget, () => [iterator, matcher, input, result, field]);
|
|
47
|
+
const read = (value, key) => {
|
|
48
|
+
if (context.getProperty !== undefined)
|
|
49
|
+
return context.getProperty(value, key);
|
|
50
|
+
const descriptor = getSandboxPropertyDescriptor(value, key, budget);
|
|
51
|
+
return descriptor === undefined ? undefined : readPropertyDescriptor(descriptor, value, context);
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
budget.visitNode();
|
|
55
|
+
result = await regexExec(matcher, input, budget, context);
|
|
56
|
+
const global = state.global ?? (isSandboxRegex(matcher) && matcher.flags.includes("g"));
|
|
57
|
+
if (result === null || !global) {
|
|
58
|
+
state.exhausted = true;
|
|
59
|
+
state.matcher = undefined;
|
|
60
|
+
state.input = undefined;
|
|
61
|
+
return result === null ? { value: undefined, done: true } : { value: result, done: false };
|
|
62
|
+
}
|
|
63
|
+
field = await read(result, "0");
|
|
64
|
+
const text = await sandboxString(field, budget, context);
|
|
65
|
+
field = undefined;
|
|
66
|
+
if (text.length === 0) {
|
|
67
|
+
field = await read(matcher, "lastIndex");
|
|
68
|
+
const index = normalizeLastIndex(await sandboxNumber(field, budget, context));
|
|
69
|
+
field = undefined;
|
|
70
|
+
const unicode = state.unicode ?? (isSandboxRegex(matcher) && (matcher.flags.includes("u") || matcher.flags.includes("v")));
|
|
71
|
+
const point = input.codePointAt(index);
|
|
72
|
+
await setSandboxProperty(matcher, "lastIndex", index + (unicode && point !== undefined && point > 0xffff ? 2 : 1), budget, true, context);
|
|
73
|
+
}
|
|
74
|
+
return { value: result, done: false };
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
release();
|
|
78
|
+
}
|
|
27
79
|
}
|
|
28
80
|
export function getRegExpIteratorMember(property, budget) {
|
|
29
81
|
if (property === Symbol.toStringTag)
|
|
@@ -43,7 +95,7 @@ export function getRegExpIteratorMember(property, budget) {
|
|
|
43
95
|
const receiver = context?.thisValue;
|
|
44
96
|
if (!isSandboxRegExpIterator(receiver))
|
|
45
97
|
throw new TypeError("RegExp string iterator next requires a matching receiver.");
|
|
46
|
-
return nextRegExpIterator(receiver, budget);
|
|
98
|
+
return context === undefined ? nextRegExpIterator(receiver, budget) : nextObservableRegExpIterator(receiver, budget, context);
|
|
47
99
|
}
|
|
48
100
|
});
|
|
49
101
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { types } from "node:util";
|
|
2
2
|
import { invokeBuiltinClosure } from "../builtin-call.js";
|
|
3
3
|
import { CompileScope } from "../regex/compile-guard.js";
|
|
4
|
-
import { normalizeLastIndex } from "../regex/engine.js";
|
|
4
|
+
import { advanceStringIndex, normalizeLastIndex } from "../regex/engine.js";
|
|
5
5
|
import { sandboxNumber, sandboxString } from "../string-coercion.js";
|
|
6
6
|
import { retainValues } from "../resources.js";
|
|
7
7
|
import { getSandboxDataProperty, getSandboxPropertyDescriptor, hasRegexPropertyOverride } from "../object-model.js";
|
|
8
8
|
import { readPropertyDescriptor } from "../accessors.js";
|
|
9
9
|
import { createSandboxBox } from "../boxed.js";
|
|
10
10
|
import { setSandboxProperty } from "../interpreter.js";
|
|
11
|
-
import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, isSandboxClosure, isSandboxRegex, measureSandboxData } from "../values.js";
|
|
11
|
+
import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, getSandboxRegexPattern, isSandboxClosure, isSandboxRegex, measureSandboxData } from "../values.js";
|
|
12
12
|
import { executeRegex, getRegexMember, regexExec, regexFlagProperties, regexSearch, toMatchArray } from "./regex.js";
|
|
13
13
|
import { restoreSandboxRegExpIterator } from "../regexp-iterator.js";
|
|
14
14
|
export const stringMethodNames = new Set([
|
|
@@ -356,7 +356,7 @@ function callReplaceLikeMethod(value, methodName, args, budget, callClosure, use
|
|
|
356
356
|
return replaceWithClosure(value, search, replacement, methodName === "replaceAll", budget, callClosure, context);
|
|
357
357
|
}
|
|
358
358
|
async function replaceRegex(value, regex, replacement, budget, callClosure, context) {
|
|
359
|
-
if (hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
|
|
359
|
+
if (getSandboxRegexPattern(regex).groups !== undefined || hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
|
|
360
360
|
return regexReplace(value, regex, replacement, budget, callClosure, context);
|
|
361
361
|
if (regex.flags.includes("g"))
|
|
362
362
|
regex.lastIndex = 0;
|
|
@@ -410,7 +410,7 @@ export async function regexReplace(input, regex, replacementInput, budget, callC
|
|
|
410
410
|
input = undefined;
|
|
411
411
|
replacement = isSandboxClosure(replacementInput) ? replacementInput : await sandboxString(replacementInput, budget, context);
|
|
412
412
|
replacementInput = undefined;
|
|
413
|
-
if (isSandboxRegex(regex) && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
|
|
413
|
+
if (isSandboxRegex(regex) && getSandboxRegexPattern(regex).groups === undefined && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
|
|
414
414
|
release();
|
|
415
415
|
return await replaceRegex(value, regex, replacement, budget, callClosure, context);
|
|
416
416
|
}
|
|
@@ -641,7 +641,7 @@ function splitNormalized(value, separator, limit, budget, parent) {
|
|
|
641
641
|
if (match === null)
|
|
642
642
|
break;
|
|
643
643
|
if (match.text.length === 0)
|
|
644
|
-
splitter.lastIndex = match.index
|
|
644
|
+
splitter.lastIndex = advanceStringIndex(value, match.index, splitter.flags.includes("u") || splitter.flags.includes("v"));
|
|
645
645
|
endedWithZeroWidthMatch = match.text.length === 0 && match.index === value.length;
|
|
646
646
|
if (match.text.length === 0 &&
|
|
647
647
|
(match.index === copiedThrough || match.index === value.length))
|
|
@@ -822,7 +822,7 @@ function callMatchLikeMethod(value, methodName, args, compilation, lastIndex) {
|
|
|
822
822
|
return match?.index ?? -1;
|
|
823
823
|
}
|
|
824
824
|
if (methodName === "match" && !regex.flags.includes("g"))
|
|
825
|
-
return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value);
|
|
825
|
+
return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value, compilation.owner?.budget);
|
|
826
826
|
if (methodName === "match")
|
|
827
827
|
regex.lastIndex = 0;
|
|
828
828
|
const matcher = methodName === "matchAll"
|
|
@@ -843,7 +843,7 @@ function collectRegexMatches(regex, value, all, budget, lastIndex = 0) {
|
|
|
843
843
|
matches.push(match);
|
|
844
844
|
lastIndex = match.index + match.text.length;
|
|
845
845
|
if (all && match.text.length === 0)
|
|
846
|
-
regex.lastIndex =
|
|
846
|
+
regex.lastIndex = lastIndex = advanceStringIndex(value, lastIndex, regex.flags.includes("u") || regex.flags.includes("v"));
|
|
847
847
|
} while (all);
|
|
848
848
|
return matches;
|
|
849
849
|
}
|
|
@@ -852,10 +852,10 @@ function splitString(value, separator, limit) {
|
|
|
852
852
|
return split.call(value, separator, limit);
|
|
853
853
|
}
|
|
854
854
|
function asNumber(value) {
|
|
855
|
-
return
|
|
855
|
+
return +value;
|
|
856
856
|
}
|
|
857
857
|
function asNumberOrUndefined(value) {
|
|
858
|
-
return value === undefined ? undefined :
|
|
858
|
+
return value === undefined ? undefined : +value;
|
|
859
859
|
}
|
|
860
860
|
function asStringOrUndefined(value) {
|
|
861
861
|
return value === undefined ? undefined : String(value);
|
|
@@ -16,6 +16,7 @@ export declare function isIntrinsicConstructor(value: object): boolean;
|
|
|
16
16
|
export declare function releaseObjectPrototype(budget: Budget): void;
|
|
17
17
|
export declare function getSandboxPrototype(value: object, budget?: Budget): object | null;
|
|
18
18
|
export declare function hasExplicitSandboxPrototype(value: object): boolean;
|
|
19
|
+
export declare function hasNullObjectPrototype(value: object): boolean;
|
|
19
20
|
export declare function getSandboxPropertyDescriptor(value: SandboxValue, key: PropertyKey, budget?: Budget): PropertyDescriptor | undefined;
|
|
20
21
|
export declare function getSandboxDataProperty(value: SandboxValue, key: PropertyKey, budget?: Budget): SandboxValue;
|
|
21
22
|
export declare function setSandboxPrototype(value: object, prototype: object | null, budget?: Budget): void;
|
|
@@ -180,6 +180,9 @@ export function getSandboxPrototype(value, budget) {
|
|
|
180
180
|
export function hasExplicitSandboxPrototype(value) {
|
|
181
181
|
return prototypes.has(value);
|
|
182
182
|
}
|
|
183
|
+
export function hasNullObjectPrototype(value) {
|
|
184
|
+
return prototypes.get(value) === null && !Array.isArray(value) && !isSandboxClosure(value) && !isSandboxRegex(value);
|
|
185
|
+
}
|
|
183
186
|
export function getSandboxPropertyDescriptor(value, key, budget) {
|
|
184
187
|
const hostProperties = isSandboxClosure(value) ? value.properties : undefined;
|
|
185
188
|
if (isSandboxClosure(value) && !isGuestClosure(value))
|
|
@@ -288,7 +291,7 @@ export function hasGuestObjectState(value) {
|
|
|
288
291
|
return !intrinsicUnchanged();
|
|
289
292
|
if (isLiveCapability(value))
|
|
290
293
|
return true;
|
|
291
|
-
if (functionProperties.has(value) || prototypes.has(value))
|
|
294
|
+
if (functionProperties.has(value) || (prototypes.has(value) && !hasNullObjectPrototype(value)))
|
|
292
295
|
return true;
|
|
293
296
|
if (isSandboxBox(value) || isSandboxDate(value))
|
|
294
297
|
return false;
|
|
@@ -121,7 +121,7 @@ export class RegexCompileGuard {
|
|
|
121
121
|
this.work(flags.length);
|
|
122
122
|
}
|
|
123
123
|
retain(pattern, valueUnits = 0) {
|
|
124
|
-
const units =
|
|
124
|
+
const units = measurePattern(pattern);
|
|
125
125
|
if (this.ticket !== undefined) {
|
|
126
126
|
this.ticket.owner.budget.resizeCompileTicket(this.ticket, units + valueUnits);
|
|
127
127
|
}
|
|
@@ -139,7 +139,16 @@ export class RegexCompileGuard {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
export function regexCompiledData(pattern) {
|
|
142
|
-
return (compiledData.get(pattern) ?? { units:
|
|
142
|
+
return (compiledData.get(pattern) ?? { units: measurePattern(pattern) });
|
|
143
|
+
}
|
|
144
|
+
function measurePattern(pattern) {
|
|
145
|
+
let units = 10 + pattern.source.length + measureNode(pattern.body);
|
|
146
|
+
if (pattern.groups !== undefined) {
|
|
147
|
+
units += 1;
|
|
148
|
+
for (const [name, indices] of Object.entries(pattern.groups))
|
|
149
|
+
units += name.length + 2 + indices.length;
|
|
150
|
+
}
|
|
151
|
+
return units;
|
|
143
152
|
}
|
|
144
153
|
function measureNode(node) {
|
|
145
154
|
switch (node.type) {
|
|
@@ -148,18 +157,27 @@ function measureNode(node) {
|
|
|
148
157
|
return 2;
|
|
149
158
|
case "literal":
|
|
150
159
|
return 3 + node.value.length;
|
|
160
|
+
case "namedBackreference":
|
|
161
|
+
return 3 + node.name.length;
|
|
151
162
|
case "anchor":
|
|
152
163
|
case "wordBoundary":
|
|
164
|
+
case "backreference":
|
|
153
165
|
return 3;
|
|
154
166
|
case "characterClass": {
|
|
155
167
|
let usage = 5 + node.items.length;
|
|
168
|
+
if (node.operation !== undefined)
|
|
169
|
+
usage += 1 + node.operation.length;
|
|
170
|
+
if (node.strings !== undefined)
|
|
171
|
+
usage += 1;
|
|
156
172
|
for (const item of node.items) {
|
|
157
173
|
usage +=
|
|
158
174
|
item.type === "character"
|
|
159
175
|
? 3 + item.value.length
|
|
160
176
|
: item.type === "range"
|
|
161
177
|
? 4 + item.from.length + item.to.length
|
|
162
|
-
: 4
|
|
178
|
+
: item.type === "property" ? 4 + item.value.length + (item.strings === undefined ? 0 : 1)
|
|
179
|
+
: item.type === "set" ? 3 + measureNode({ type: "characterClass", ...item.value })
|
|
180
|
+
: item.type === "strings" ? 4 + item.values.reduce((total, value) => total + 1 + value.length, 0) : 4;
|
|
163
181
|
}
|
|
164
182
|
return usage;
|
|
165
183
|
}
|
|
@@ -172,7 +190,11 @@ function measureNode(node) {
|
|
|
172
190
|
return usage;
|
|
173
191
|
}
|
|
174
192
|
case "group":
|
|
175
|
-
return 5 + measureNode(node.body)
|
|
193
|
+
return 5 + measureNode(node.body) + (node.name === undefined ? 0 : 1 + node.name.length) +
|
|
194
|
+
(node.modifiers === undefined ? 0 : 2 + Object.keys(node.modifiers).length);
|
|
195
|
+
case "lookahead":
|
|
196
|
+
case "lookbehind":
|
|
197
|
+
return 4 + measureNode(node.body);
|
|
176
198
|
case "quantifier":
|
|
177
199
|
return (Object.hasOwn(node, "max") ? 6 : 5) + measureNode(node.body);
|
|
178
200
|
}
|
|
@@ -3,7 +3,11 @@ export type RegexMatch = {
|
|
|
3
3
|
index: number;
|
|
4
4
|
text: string;
|
|
5
5
|
captures: (string | undefined)[];
|
|
6
|
+
indices?: ([number, number] | undefined)[];
|
|
7
|
+
groups?: Record<string, string | undefined>;
|
|
8
|
+
indicesGroups?: Record<string, [number, number] | undefined>;
|
|
6
9
|
};
|
|
7
10
|
export declare function matchRegex(pattern: RegexPattern, input: string, lastIndex?: number): RegexMatch | null;
|
|
8
11
|
export declare function matchRegexFrom(pattern: RegexPattern, input: string, startIndex: number): RegexMatch | null;
|
|
9
12
|
export declare function normalizeLastIndex(lastIndex: number): number;
|
|
13
|
+
export declare function advanceStringIndex(input: string, index: number, unicode: boolean): number;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { allocateRegexSteps } from "../budget.js";
|
|
2
2
|
export function matchRegex(pattern, input, lastIndex = 0) {
|
|
3
|
-
const startIndex = pattern.flags.global ? normalizeLastIndex(lastIndex) : 0;
|
|
3
|
+
const startIndex = pattern.flags.global || pattern.flags.sticky ? normalizeLastIndex(lastIndex) : 0;
|
|
4
4
|
return matchRegexFrom(pattern, input, startIndex);
|
|
5
5
|
}
|
|
6
6
|
export function matchRegexFrom(pattern, input, startIndex) {
|
|
7
7
|
if (startIndex > input.length) {
|
|
8
8
|
return null;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const unicode = pattern.flags.unicode || pattern.flags.unicodeSets;
|
|
11
|
+
if (unicode && startIndex > 0 && startIndex < input.length &&
|
|
12
|
+
input.charCodeAt(startIndex) >= 0xdc00 && input.charCodeAt(startIndex) <= 0xdfff &&
|
|
13
|
+
input.charCodeAt(startIndex - 1) >= 0xd800 && input.charCodeAt(startIndex - 1) <= 0xdbff)
|
|
14
|
+
startIndex--;
|
|
15
|
+
for (let attempt = startIndex; attempt <= input.length; attempt = advanceStringIndex(input, attempt, unicode)) {
|
|
16
|
+
const context = { input, flags: pattern.flags, groups: pattern.groups, direction: 1, work: { steps: 0 } };
|
|
12
17
|
charge(context);
|
|
13
18
|
const initialState = {
|
|
14
19
|
position: attempt,
|
|
@@ -16,26 +21,63 @@ export function matchRegexFrom(pattern, input, startIndex) {
|
|
|
16
21
|
};
|
|
17
22
|
const result = matchNode(pattern.body, initialState, context).next();
|
|
18
23
|
if (!result.done) {
|
|
19
|
-
return toRegexMatch(input, attempt, result.value);
|
|
24
|
+
return toRegexMatch(input, attempt, result.value, pattern.flags.hasIndices, pattern.groups);
|
|
20
25
|
}
|
|
26
|
+
if (pattern.flags.sticky)
|
|
27
|
+
break;
|
|
21
28
|
}
|
|
22
29
|
return null;
|
|
23
30
|
}
|
|
24
31
|
function* matchNode(node, state, context) {
|
|
25
32
|
charge(context);
|
|
33
|
+
const unicode = context.flags.unicode || context.flags.unicodeSets;
|
|
34
|
+
const character = readCharacter(context.input, state.position, context.direction, unicode);
|
|
26
35
|
switch (node.type) {
|
|
27
36
|
case "empty":
|
|
28
37
|
yield state;
|
|
29
38
|
return;
|
|
30
39
|
case "literal":
|
|
31
|
-
if (charactersEqual(
|
|
32
|
-
yield { ...state, position: state.position +
|
|
40
|
+
if (charactersEqual(character, node.value, context.flags.ignoreCase, unicode)) {
|
|
41
|
+
yield { ...state, position: state.position + context.direction * character.length };
|
|
33
42
|
}
|
|
34
43
|
return;
|
|
44
|
+
case "backreference":
|
|
45
|
+
case "namedBackreference": {
|
|
46
|
+
let capture;
|
|
47
|
+
if (node.type === "backreference")
|
|
48
|
+
capture = state.captures[node.index - 1];
|
|
49
|
+
else {
|
|
50
|
+
for (const index of context.groups?.[node.name] ?? []) {
|
|
51
|
+
charge(context);
|
|
52
|
+
if (state.captures[index - 1] !== undefined) {
|
|
53
|
+
capture = state.captures[index - 1];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (capture === undefined) {
|
|
59
|
+
yield state;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let position = state.position;
|
|
63
|
+
let reference = context.direction === 1 ? capture.start : capture.end;
|
|
64
|
+
const end = context.direction === 1 ? capture.end : capture.start;
|
|
65
|
+
while (reference !== end) {
|
|
66
|
+
charge(context);
|
|
67
|
+
const expected = readCharacter(context.input, reference, context.direction, unicode);
|
|
68
|
+
const actual = readCharacter(context.input, position, context.direction, unicode);
|
|
69
|
+
if (!charactersEqual(actual, expected, context.flags.ignoreCase, unicode))
|
|
70
|
+
return;
|
|
71
|
+
reference += context.direction * expected.length;
|
|
72
|
+
position += context.direction * actual.length;
|
|
73
|
+
}
|
|
74
|
+
yield { ...state, position };
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
35
77
|
case "dot":
|
|
36
|
-
if (
|
|
37
|
-
(context.flags.dotAll || !isLineTerminator(
|
|
38
|
-
yield { ...state, position: state.position +
|
|
78
|
+
if (character !== undefined &&
|
|
79
|
+
(context.flags.dotAll || !isLineTerminator(character))) {
|
|
80
|
+
yield { ...state, position: state.position + context.direction * character.length };
|
|
39
81
|
}
|
|
40
82
|
return;
|
|
41
83
|
case "anchor":
|
|
@@ -44,18 +86,25 @@ function* matchNode(node, state, context) {
|
|
|
44
86
|
}
|
|
45
87
|
return;
|
|
46
88
|
case "wordBoundary": {
|
|
47
|
-
const previousWord = state.position > 0 &&
|
|
48
|
-
const nextWord = state.position < context.input.length &&
|
|
89
|
+
const previousWord = state.position > 0 && matchesCharacterClassItem(context.input[state.position - 1], { type: "kind", kind: "word", negated: false }, context.flags.ignoreCase, unicode);
|
|
90
|
+
const nextWord = state.position < context.input.length && matchesCharacterClassItem(context.input[state.position], { type: "kind", kind: "word", negated: false }, context.flags.ignoreCase, unicode);
|
|
49
91
|
if ((previousWord !== nextWord) !== node.negated) {
|
|
50
92
|
yield state;
|
|
51
93
|
}
|
|
52
94
|
return;
|
|
53
95
|
}
|
|
54
96
|
case "characterClass": {
|
|
55
|
-
|
|
97
|
+
if (context.flags.unicodeSets) {
|
|
98
|
+
const lengths = [...matchUnicodeSet(node, state.position, context)].sort((a, b) => b - a);
|
|
99
|
+
for (const length of lengths) {
|
|
100
|
+
charge(context);
|
|
101
|
+
yield { ...state, position: state.position + context.direction * length };
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
56
105
|
if (character !== undefined &&
|
|
57
|
-
matchesCharacterClass(character, node.items, node.negated, context
|
|
58
|
-
yield { ...state, position: state.position +
|
|
106
|
+
matchesCharacterClass(character, node.items, node.negated, context)) {
|
|
107
|
+
yield { ...state, position: state.position + context.direction * character.length };
|
|
59
108
|
}
|
|
60
109
|
return;
|
|
61
110
|
}
|
|
@@ -67,14 +116,30 @@ function* matchNode(node, state, context) {
|
|
|
67
116
|
yield* matchNode(alternative, cloneState(state), context);
|
|
68
117
|
}
|
|
69
118
|
return;
|
|
119
|
+
case "lookahead":
|
|
120
|
+
case "lookbehind": {
|
|
121
|
+
const result = matchNode(node.body, cloneState(state), {
|
|
122
|
+
...context, direction: node.type === "lookbehind" ? -1 : 1
|
|
123
|
+
}).next();
|
|
124
|
+
if (node.negated) {
|
|
125
|
+
if (result.done)
|
|
126
|
+
yield state;
|
|
127
|
+
}
|
|
128
|
+
else if (!result.done) {
|
|
129
|
+
yield { position: state.position, captures: result.value.captures };
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
70
133
|
case "group":
|
|
71
|
-
for (const result of matchNode(node.body, cloneState(state), context
|
|
134
|
+
for (const result of matchNode(node.body, cloneState(state), node.modifiers === undefined ? context : {
|
|
135
|
+
...context, flags: { ...context.flags, ...node.modifiers }
|
|
136
|
+
})) {
|
|
72
137
|
if (!node.capturing || node.index === undefined) {
|
|
73
138
|
yield result;
|
|
74
139
|
continue;
|
|
75
140
|
}
|
|
76
141
|
const captures = result.captures.slice();
|
|
77
|
-
captures[node.index - 1] = { start: state.position, end: result.position };
|
|
142
|
+
captures[node.index - 1] = { start: Math.min(state.position, result.position), end: Math.max(state.position, result.position) };
|
|
78
143
|
yield { position: result.position, captures };
|
|
79
144
|
}
|
|
80
145
|
return;
|
|
@@ -88,7 +153,8 @@ function* matchSequence(elements, index, state, context) {
|
|
|
88
153
|
yield state;
|
|
89
154
|
return;
|
|
90
155
|
}
|
|
91
|
-
|
|
156
|
+
const element = elements[context.direction === 1 ? index : elements.length - 1 - index];
|
|
157
|
+
for (const result of matchNode(element, state, context)) {
|
|
92
158
|
yield* matchSequence(elements, index + 1, result, context);
|
|
93
159
|
}
|
|
94
160
|
}
|
|
@@ -129,11 +195,35 @@ function matchesAnchor(kind, position, context) {
|
|
|
129
195
|
position < context.input.length &&
|
|
130
196
|
isLineTerminator(context.input[position])));
|
|
131
197
|
}
|
|
132
|
-
function matchesCharacterClass(character, items, negated,
|
|
133
|
-
const matched = items.some((item) =>
|
|
198
|
+
function matchesCharacterClass(character, items, negated, context) {
|
|
199
|
+
const matched = items.some((item) => {
|
|
200
|
+
if (context.flags.unicode)
|
|
201
|
+
charge(context);
|
|
202
|
+
return matchesCharacterClassItem(character, item, context.flags.ignoreCase, context.flags.unicode);
|
|
203
|
+
});
|
|
134
204
|
return negated ? !matched : matched;
|
|
135
205
|
}
|
|
136
|
-
function matchesCharacterClassItem(character, item, ignoreCase) {
|
|
206
|
+
function matchesCharacterClassItem(character, item, ignoreCase, unicode, unicodeSets = false) {
|
|
207
|
+
if (item.type === "strings" || item.type === "set")
|
|
208
|
+
return false;
|
|
209
|
+
if (unicode) {
|
|
210
|
+
// Generated atoms only: the host classifies one code point, never executes a guest pattern.
|
|
211
|
+
const hex = (value) => `\\u{${value.codePointAt(0).toString(16)}}`;
|
|
212
|
+
let atom;
|
|
213
|
+
if (item.type === "character")
|
|
214
|
+
atom = hex(item.value);
|
|
215
|
+
else if (item.type === "range")
|
|
216
|
+
atom = `[${hex(item.from)}-${hex(item.to)}]`;
|
|
217
|
+
else if (item.type === "property")
|
|
218
|
+
atom = `\\${item.negated ? "P" : "p"}{${item.value}}`;
|
|
219
|
+
else {
|
|
220
|
+
const kind = item.kind === "digit" ? "d" : item.kind === "word" ? "w" : "s";
|
|
221
|
+
atom = `\\${item.negated ? kind.toUpperCase() : kind}`;
|
|
222
|
+
}
|
|
223
|
+
return new RegExp(`^(?:${atom})$`, (ignoreCase ? "i" : "") + (unicodeSets ? "v" : "u")).test(character);
|
|
224
|
+
}
|
|
225
|
+
if (item.type === "property")
|
|
226
|
+
return false;
|
|
137
227
|
if (item.type === "character") {
|
|
138
228
|
return charactersEqual(character, item.value, ignoreCase);
|
|
139
229
|
}
|
|
@@ -147,10 +237,10 @@ function matchesCharacterClassItem(character, item, ignoreCase) {
|
|
|
147
237
|
if (!ignoreCase) {
|
|
148
238
|
return false;
|
|
149
239
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
240
|
+
// Folding endpoints does not preserve range membership. Classify one UTF-16
|
|
241
|
+
// code unit using generated hex endpoints, never a guest pattern.
|
|
242
|
+
return new RegExp(`^[\\u${from.toString(16).padStart(4, "0")}-\\u${to.toString(16).padStart(4, "0")}]$`, "i")
|
|
243
|
+
.test(character);
|
|
154
244
|
}
|
|
155
245
|
const matched = item.kind === "digit"
|
|
156
246
|
? isDigit(character)
|
|
@@ -159,12 +249,107 @@ function matchesCharacterClassItem(character, item, ignoreCase) {
|
|
|
159
249
|
: isSpaceCharacter(character);
|
|
160
250
|
return item.negated ? !matched : matched;
|
|
161
251
|
}
|
|
162
|
-
function
|
|
163
|
-
|
|
252
|
+
function matchUnicodeSet(set, position, context) {
|
|
253
|
+
charge(context);
|
|
254
|
+
let lengths = new Set();
|
|
255
|
+
for (let index = 0; index < set.items.length; index++) {
|
|
256
|
+
const next = matchUnicodeSetItem(set.items[index], position, context);
|
|
257
|
+
if (index === 0)
|
|
258
|
+
lengths = next;
|
|
259
|
+
else if (set.operation === undefined) {
|
|
260
|
+
for (const length of next) {
|
|
261
|
+
charge(context);
|
|
262
|
+
lengths.add(length);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
for (const length of lengths) {
|
|
267
|
+
charge(context);
|
|
268
|
+
if (set.operation === "intersection" ? !next.has(length) : next.has(length))
|
|
269
|
+
lengths.delete(length);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (set.negated) {
|
|
274
|
+
const character = readCharacter(context.input, position, context.direction, true);
|
|
275
|
+
return new Set(character !== undefined && !lengths.has(character.length) ? [character.length] : []);
|
|
276
|
+
}
|
|
277
|
+
return lengths;
|
|
278
|
+
}
|
|
279
|
+
function matchUnicodeSetItem(item, position, context) {
|
|
280
|
+
charge(context);
|
|
281
|
+
if (item.type === "set")
|
|
282
|
+
return matchUnicodeSet(item.value, position, context);
|
|
283
|
+
const lengths = new Set();
|
|
284
|
+
if (item.type === "strings") {
|
|
285
|
+
for (const value of item.values) {
|
|
286
|
+
charge(context);
|
|
287
|
+
let reference = context.direction === 1 ? 0 : value.length;
|
|
288
|
+
let cursor = position;
|
|
289
|
+
const end = context.direction === 1 ? value.length : 0;
|
|
290
|
+
while (reference !== end) {
|
|
291
|
+
charge(context);
|
|
292
|
+
const expected = readCharacter(value, reference, context.direction, true);
|
|
293
|
+
const actual = readCharacter(context.input, cursor, context.direction, true);
|
|
294
|
+
if (!charactersEqual(actual, expected, context.flags.ignoreCase, true))
|
|
295
|
+
break;
|
|
296
|
+
reference += context.direction * expected.length;
|
|
297
|
+
cursor += context.direction * actual.length;
|
|
298
|
+
}
|
|
299
|
+
if (reference === end)
|
|
300
|
+
lengths.add(Math.abs(cursor - position));
|
|
301
|
+
}
|
|
302
|
+
return lengths;
|
|
303
|
+
}
|
|
304
|
+
if (item.type === "property" && item.strings) {
|
|
305
|
+
// A validated property atom denotes a finite Unicode string set. The host
|
|
306
|
+
// finds its longest member only; guest alternation/backtracking stays here.
|
|
307
|
+
const atom = `\\p{${item.value}}`;
|
|
308
|
+
const flags = (context.flags.ignoreCase ? "i" : "") + "v";
|
|
309
|
+
const matcher = new RegExp(context.direction === 1 ? atom : `(?<=(${atom}))`, flags + "y");
|
|
310
|
+
matcher.lastIndex = position;
|
|
311
|
+
const match = matcher.exec(context.input);
|
|
312
|
+
if (match === null)
|
|
313
|
+
return lengths;
|
|
314
|
+
const longest = context.direction === 1 ? match[0] : match[1];
|
|
315
|
+
const member = new RegExp(`^(?:${atom})$`, flags);
|
|
316
|
+
let consumed = 0;
|
|
317
|
+
while (consumed < longest.length) {
|
|
318
|
+
charge(context);
|
|
319
|
+
const cursor = context.direction === 1 ? consumed : longest.length - consumed;
|
|
320
|
+
consumed += readCharacter(longest, cursor, context.direction, true).length;
|
|
321
|
+
const candidate = context.direction === 1 ? longest.slice(0, consumed) : longest.slice(longest.length - consumed);
|
|
322
|
+
if (member.test(candidate))
|
|
323
|
+
lengths.add(consumed);
|
|
324
|
+
}
|
|
325
|
+
return lengths;
|
|
326
|
+
}
|
|
327
|
+
const character = readCharacter(context.input, position, context.direction, true);
|
|
328
|
+
if (character !== undefined && matchesCharacterClassItem(character, item, context.flags.ignoreCase, true, true))
|
|
329
|
+
lengths.add(character.length);
|
|
330
|
+
return lengths;
|
|
331
|
+
}
|
|
332
|
+
function toRegexMatch(input, start, state, hasIndices, groups) {
|
|
333
|
+
const match = {
|
|
164
334
|
index: start,
|
|
165
335
|
text: input.slice(start, state.position),
|
|
166
336
|
captures: state.captures.map((capture) => capture === undefined ? undefined : input.slice(capture.start, capture.end))
|
|
167
337
|
};
|
|
338
|
+
if (hasIndices) {
|
|
339
|
+
match.indices = [[start, state.position], ...state.captures.map((capture) => capture === undefined ? undefined : [capture.start, capture.end])];
|
|
340
|
+
}
|
|
341
|
+
if (groups !== undefined) {
|
|
342
|
+
match.groups = Object.create(null);
|
|
343
|
+
if (hasIndices)
|
|
344
|
+
match.indicesGroups = Object.create(null);
|
|
345
|
+
for (const [name, indices] of Object.entries(groups)) {
|
|
346
|
+
const index = indices.find(index => state.captures[index - 1] !== undefined);
|
|
347
|
+
match.groups[name] = index === undefined ? undefined : match.captures[index - 1];
|
|
348
|
+
if (match.indicesGroups !== undefined)
|
|
349
|
+
match.indicesGroups[name] = index === undefined ? undefined : match.indices[index];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return match;
|
|
168
353
|
}
|
|
169
354
|
function cloneState(state) {
|
|
170
355
|
return { position: state.position, captures: state.captures.slice() };
|
|
@@ -194,13 +379,13 @@ function clearNodeCaptures(node, captures) {
|
|
|
194
379
|
}
|
|
195
380
|
return;
|
|
196
381
|
}
|
|
197
|
-
if (node.type === "quantifier") {
|
|
382
|
+
if (node.type === "quantifier" || node.type === "lookahead" || node.type === "lookbehind") {
|
|
198
383
|
clearNodeCaptures(node.body, captures);
|
|
199
384
|
}
|
|
200
385
|
}
|
|
201
386
|
function charge(context) {
|
|
202
|
-
context.steps += 1;
|
|
203
|
-
allocateRegexSteps(context.steps);
|
|
387
|
+
context.work.steps += 1;
|
|
388
|
+
allocateRegexSteps(context.work.steps);
|
|
204
389
|
}
|
|
205
390
|
export function normalizeLastIndex(lastIndex) {
|
|
206
391
|
if (Number.isNaN(lastIndex) || lastIndex <= 0) {
|
|
@@ -208,9 +393,24 @@ export function normalizeLastIndex(lastIndex) {
|
|
|
208
393
|
}
|
|
209
394
|
return Math.min(Math.floor(lastIndex), Number.MAX_SAFE_INTEGER);
|
|
210
395
|
}
|
|
211
|
-
function charactersEqual(left, right, ignoreCase) {
|
|
396
|
+
function charactersEqual(left, right, ignoreCase, unicode = false) {
|
|
397
|
+
if (unicode && ignoreCase && left !== undefined)
|
|
398
|
+
return new RegExp(`^\\u{${right.codePointAt(0).toString(16)}}$`, "iu").test(left);
|
|
212
399
|
return left !== undefined && foldCharacter(left, ignoreCase) === foldCharacter(right, ignoreCase);
|
|
213
400
|
}
|
|
401
|
+
export function advanceStringIndex(input, index, unicode) {
|
|
402
|
+
return index + (unicode && (input.codePointAt(index) ?? 0) > 0xffff ? 2 : 1);
|
|
403
|
+
}
|
|
404
|
+
function readCharacter(input, position, direction, unicode) {
|
|
405
|
+
let index = direction === 1 ? position : position - 1;
|
|
406
|
+
if (index < 0 || index >= input.length)
|
|
407
|
+
return undefined;
|
|
408
|
+
if (unicode && direction === -1 && index > 0 &&
|
|
409
|
+
input.charCodeAt(index) >= 0xdc00 && input.charCodeAt(index) <= 0xdfff &&
|
|
410
|
+
input.charCodeAt(index - 1) >= 0xd800 && input.charCodeAt(index - 1) <= 0xdbff)
|
|
411
|
+
index--;
|
|
412
|
+
return unicode ? String.fromCodePoint(input.codePointAt(index)) : input[index];
|
|
413
|
+
}
|
|
214
414
|
function foldCharacter(character, ignoreCase) {
|
|
215
415
|
if (!ignoreCase) {
|
|
216
416
|
return character;
|