regex-partial-match 1.0.0 β 1.1.1
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/README.md +92 -23
- package/lib/compilePartial.d.ts +4 -1
- package/lib/compilePartial.js +100 -22
- package/lib/partialMatchRegExp.d.ts +3 -0
- package/lib/partialMatchRegExp.js +45 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Unlike C/C++ (via [PCRE/PCRE2](https://www.pcre.org/original/doc/html/pcrepartia
|
|
|
10
10
|
|
|
11
11
|
This library transforms regular expressions to best-effort support **partial matching**, allowing you to test if an incomplete string could potentially match the full pattern. This is particularly useful for real-time input validation, autocomplete systems, progressive form validation, stream chunk matching, etc.
|
|
12
12
|
|
|
13
|
+
As a side effect of the parse this requires, each `PartialMatchRegExp` also exposes a [`features`](#partialmatchregexpprototypefeatures-readonlysetregexfeature) set naming the syntactic constructs its pattern uses β useful for consumers that need to reason about a pattern without writing their own regex parser. For many features, a simple search in the [source](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source) would be insufficient.
|
|
14
|
+
|
|
13
15
|
**Based on an algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski)**, re-created for NPM via ISC license, with permission.
|
|
14
16
|
|
|
15
17
|
## π¦ Installation
|
|
@@ -54,10 +56,6 @@ The library transforms a regular expression by wrapping each [atomic element](ht
|
|
|
54
56
|
|
|
55
57
|
This allows the pattern to match prefixes of the original pattern, enabling validation of incomplete input.
|
|
56
58
|
|
|
57
|
-
### Patterns with backreferences
|
|
58
|
-
|
|
59
|
-
Backreferences cannot be handled by the `|$(?![\s\S])` transform alone because they are atomic β `\1` must match the entire captured string or fail, and its length is only known at runtime. `PartialMatchRegExp` first tries a full match natively, as a short-circuit β if the input already satisfies the whole pattern, there's nothing further to do. Otherwise it runs a "capture scan": a variant of the pattern with each backreference swapped for a lazy `(?:[\s\S]*?)` wildcard, so the group it depends on can still capture against a partial input β matching anything, or nothing at all, without needing to already know the backreference's value. Whatever that scan captures (or leaves undefined, if the group hasn't been reached yet) is then used to build a fresh partial-matching regex for this specific input, expanding the backreference character-by-character from the captured value with the same per-atom transform as the rest of the pattern. See [docs/backreferences.md](./docs/backreferences.md) for the full algorithm, including the prefer-longer post-processing that preserves correct captures for groups inside quantifiers.
|
|
60
|
-
|
|
61
59
|
Since the library accepts only valid regular expressions [^2], this enables the algorithm to make lots of unguarded assumptions about the source of the expression.
|
|
62
60
|
|
|
63
61
|
The library has been stress-tested with various regular expression features in isolation, and some in likely combination, but obviously it's an unbounded test space, and syntactically valid regular expressions nevertheless support contradictory patterns e.g.
|
|
@@ -71,6 +69,13 @@ Such combinations have not been tested.
|
|
|
71
69
|
> [!NOTE]
|
|
72
70
|
> See [Partial Match Parity](/docs/partial-match-parity.md) for full details on how the library compares to reference implementations
|
|
73
71
|
|
|
72
|
+
### Patterns with backreferences
|
|
73
|
+
|
|
74
|
+
Backreferences cannot be handled by the `|$(?![\s\S])` transform alone because they are atomic β `\1` must match the entire captured string or fail, and its length is only known at runtime. `PartialMatchRegExp` first tries a full match natively, but that native result only wins outright if nothing earlier in the input could still be a viable partial β a cheap bound check settles that without needing to resolve the backreference's actual value, so the common case (no earlier partial exists) stays fast. Otherwise it runs a "capture scan": a variant of the pattern with each backreference swapped for a lazy `(?:[\s\S]*?)` wildcard, so the group it depends on can still capture against a partial input β matching anything, or nothing at all, without needing to already know the backreference's value.
|
|
75
|
+
|
|
76
|
+
Whatever that scan captures (or leaves `undefined`, if the group hasn't been reached yet) is then used to build a fresh partial-matching regex for this specific input, expanding the backreference character-by-character from the captured value with the same per-atom transform as the rest of the pattern. See [docs/backreferences.md](./docs/backreferences.md) for the full algorithm, including the prefer-longer post-processing that preserves correct captures for groups inside quantifiers.
|
|
77
|
+
|
|
78
|
+
|
|
74
79
|
## β
Supported Features
|
|
75
80
|
|
|
76
81
|
- π€ [Literal characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Literal_character)
|
|
@@ -171,17 +176,36 @@ The following cases remain atomic (full native value or exactly at true end of i
|
|
|
171
176
|
- **`\k<name>` with no named capturing groups in the pattern.** [Annex B](https://tc39.es/ecma262/#sec-regular-expressions-patterns) tolerates this as the literal characters `k<a>`, but it's still treated as if it were a named backreference and matched atomically β `"k"` and `"k<"` will not partially match. A `\k` not immediately followed by a well-formed `<name>` reference is treated as the literal `k` and partially matches as usual.
|
|
172
177
|
- **A backreference whose captured value can't be determined from a partial input.** This only affects the backreference site itself; it's strictly better than rejecting the input outright, and never accepts anything unsound.
|
|
173
178
|
|
|
174
|
-
|
|
179
|
+
#### Prefix-ambiguous top-level alternation
|
|
180
|
+
|
|
181
|
+
When a pattern uses top-level alternation where one branch is a strict prefix of another (e.g. `^(ab)\1|^(abc)\2`), the internal capture scan may select the shorter branch β because it uses `(?:[\s\S]*?)` which accepts zero characters β causing the final partial regex to fail for inputs that are valid prefixes of the longer branch. In such cases `exec` returns `null` even though the input is a valid partial match:
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
const partial = new PartialMatchRegExp(/^(ab)\1|^(abc)\2/);
|
|
185
|
+
|
|
186
|
+
partial.test("abca"); // false β but "abca" is a valid prefix of "abcabc" via the second branch
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> [!TIP]
|
|
190
|
+
> If alternate branches share a prefix, list the longer one first. The capture scan tries branches in order and stops at the first that accepts the partial input, so putting the longer branch first ensures it's the one selected:
|
|
191
|
+
>
|
|
192
|
+
> ```javascript
|
|
193
|
+
> const partial = new PartialMatchRegExp(/^(abc)\2|^(ab)\1/);
|
|
194
|
+
>
|
|
195
|
+
> partial.test("abca"); // true
|
|
196
|
+
> ```
|
|
197
|
+
|
|
198
|
+
See [docs/backreferences.md](./docs/backreferences.md) for why this happens (the internal capture scan resolving the wrong alternative first).
|
|
175
199
|
|
|
176
|
-
[^1]:
|
|
177
|
-
|
|
200
|
+
[^1]:
|
|
201
|
+
A bare `$` alone isn't sufficient here: under the `m` (multiline) flag β including one turned on locally via a `(?m:...)` [modifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) β `$` also matches immediately before *any* line terminator, not just the true end of input. That would let a `"\n"` the source pattern never allowed for be silently accepted as if the input had simply run out, e.g. `new PartialMatchRegExp(/^foobar/m)` would wrongly accept `"foo\nbaz"`. Appending `(?![\s\S])` narrows the disjunction down to strict end-of-input, regardless of multiline state.
|
|
178
202
|
|
|
179
|
-
|
|
203
|
+
See [chromium issue 536420076](https://issues.chromium.org/u/2/issues/536420076) for the underlying V8 bug that requires `$` to precede `(?![\s\S])` rather than using the lookahead alone.
|
|
180
204
|
|
|
181
|
-
|
|
205
|
+
A shorter option, `(?-m:$)` β disabling multiline locally so `$` means strict end-of-input on its own β also sidesteps the bug and saves a few bytes per atom. However, modifier groups are new enough that support isn't universal, and feature-detecting them would add a fallback branch the test suite can't exercise honestly, since every engine that can realistically be tested against already supports them.
|
|
182
206
|
|
|
183
|
-
[^2]:
|
|
184
|
-
|
|
207
|
+
[^2]:
|
|
208
|
+
To remain lightweight, no runtime type validation is applied, so non-TypeScript consumers will be reliant on underlying errors thrown if used incorrectly.
|
|
185
209
|
|
|
186
210
|
### Positive Lookbehinds
|
|
187
211
|
|
|
@@ -238,18 +262,6 @@ Hence, `[\p{RGI_Emoji_Flag_Sequence}]` will match `πΊπ³` as a whole, but not
|
|
|
238
262
|
|
|
239
263
|
In [`v` mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) expressions, where `[\q{abc}]` syntax is used in isolation (rather than its canonical use-case as a subtraction/intersection of another character class), this will also only match entirely or not at all. i.e. `abc` can match, but not partially.
|
|
240
264
|
|
|
241
|
-
### Prefix-ambiguous top-level alternation
|
|
242
|
-
|
|
243
|
-
When a pattern uses top-level alternation where one branch is a strict prefix of another (e.g. `^(ab)\1|^(abc)\2`), the internal capture scan may select the shorter branch β because it uses `(?:[\s\S]*?)` which accepts zero characters β causing the final partial regex to fail for inputs that are valid prefixes of the longer branch. In such cases `exec` returns `null` even though the input is a valid partial match:
|
|
244
|
-
|
|
245
|
-
```javascript
|
|
246
|
-
const partial = new PartialMatchRegExp(/^(ab)\1|^(abc)\2/);
|
|
247
|
-
|
|
248
|
-
partial.test("abca"); // false β but "abca" is a valid prefix of "abcabc" via the second branch
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
See [docs/backreferences.md](./docs/backreferences.md) for why this happens (the internal capture scan resolving the wrong alternative first).
|
|
252
|
-
|
|
253
265
|
## π‘ Examples
|
|
254
266
|
|
|
255
267
|
### Form Validation
|
|
@@ -352,6 +364,63 @@ When using `import 'regex-partial-match/extend'`, this method is added to `RegEx
|
|
|
352
364
|
|
|
353
365
|
- A new `PartialMatchRegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
|
|
354
366
|
|
|
367
|
+
### `PartialMatchRegExp.prototype.features: ReadonlySet<RegexFeature>`
|
|
368
|
+
|
|
369
|
+
Building the partial-match regex requires walking the entire source pattern once. As a side effect of that same walk, each instance records which syntactic constructs its pattern actually uses, exposed as a `features` set β no separate scan of the source is performed to produce it.
|
|
370
|
+
|
|
371
|
+
This is useful for consumers building on top of `PartialMatchRegExp` who need to reason about which constructs a *specific* pattern uses, without writing their own regex parser to find out. Two concrete cases:
|
|
372
|
+
|
|
373
|
+
- **Flagging patterns likely to hit one of the [caveats](#caveats) documented above.** For example, a pattern combining `backreference` with `lookbehind`, `negativeLookahead`, or `negativeLookbehind` is a candidate for the [atomic-backreference caveat](#backreferences); one combining `backreference` with `disjunction` is a candidate for the [prefix-ambiguous top-level alternation caveat](#prefix-ambiguous-top-level-alternation). A consumer accepting user-supplied patterns can surface a warning instead of letting the edge case surprise someone later.
|
|
374
|
+
- **Restricting which constructs a product surface allows.** e.g. a system that only wants to accept "simple" patterns (no lookaround, no backreferences) from untrusted input can check `features` against an allow-list and reject the rest, without needing to hand-roll that check against the raw pattern source.
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
import PartialMatchRegExp from "regex-partial-match";
|
|
378
|
+
|
|
379
|
+
const partial = new PartialMatchRegExp(/^[a-z]+(?<domain>\.[a-z]+)\1/);
|
|
380
|
+
|
|
381
|
+
partial.features; // Set { "startAnchor", "characterClass", "quantifier", "namedGroup", "capturingGroup", "backreference" }
|
|
382
|
+
partial.features.has("backreference"); // true
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`RegexFeature` is a string union, exported alongside `PartialMatchRegExp`:
|
|
386
|
+
|
|
387
|
+
| Feature | Matches | Notes |
|
|
388
|
+
| --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
389
|
+
| `patternCharacter` | An ordinary literal character | |
|
|
390
|
+
| `startAnchor` | Top-level `^` | |
|
|
391
|
+
| `endAnchor` | Top-level `$` | |
|
|
392
|
+
| `wordBoundary` | Top-level `\b` | |
|
|
393
|
+
| `nonWordBoundary` | Top-level `\B` | |
|
|
394
|
+
| `lookahead` | `(?=...)` | |
|
|
395
|
+
| `negativeLookahead` | `(?!...)` | |
|
|
396
|
+
| `lookbehind` | `(?<=...)` | |
|
|
397
|
+
| `negativeLookbehind` | `(?<!...)` | |
|
|
398
|
+
| `backreference` | `\1` | |
|
|
399
|
+
| `namedBackreference` | `\k<name>` | |
|
|
400
|
+
| `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` β see below |
|
|
401
|
+
| `capturingGroup` | `(...)`, including named groups | |
|
|
402
|
+
| `nonCapturingGroup` | `(?:...)` | |
|
|
403
|
+
| `modifierGroup` | `(?ims:...)` | |
|
|
404
|
+
| `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
|
|
405
|
+
| `characterClass` | `[...]` | |
|
|
406
|
+
| `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
|
|
407
|
+
| `classIntersection` | `&&` inside a character class | `v` flag only |
|
|
408
|
+
| `classSubtraction` | `--` inside a character class | `v` flag only |
|
|
409
|
+
| `disjunction` | `\|` | |
|
|
410
|
+
| `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
|
|
411
|
+
| `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only β otherwise tagged `otherEscape` |
|
|
412
|
+
| `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
|
|
413
|
+
| `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
|
|
414
|
+
| `controlLetterEscape` | `\cX` | |
|
|
415
|
+
| `hexEscapeSequence` | `\xXX` | |
|
|
416
|
+
| `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
|
|
417
|
+
| `otherEscape` | Any other `\X`, e.g. `\.`, `\0` | |
|
|
418
|
+
|
|
419
|
+
Two things worth knowing about how these tags line up with the grammar:
|
|
420
|
+
|
|
421
|
+
- **One ECMA-262 production can map to several tags.** `Assertion` alone covers `^`, `$`, `\b`, `\B`, and all four lookarounds β `features` splits it by whichever discriminant is easiest to read off during the walk (`^` vs `$`, `=` vs `!` after `(?<`, etc.), since that information is free at the point each construct is recognised.
|
|
422
|
+
- **A named capturing group always carries both `namedGroup` and `capturingGroup`.** The grammar treats a capturing group with a name and one without as the same production (`( GroupSpecifier? Disjunction )`), not two, so both tags are added together.
|
|
423
|
+
|
|
355
424
|
## π License
|
|
356
425
|
|
|
357
426
|
ISC License - see [LICENSE](./LICENSE) file for details.
|
package/lib/compilePartial.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
export type RegexFeature = "patternCharacter" | "startAnchor" | "endAnchor" | "wordBoundary" | "nonWordBoundary" | "lookahead" | "negativeLookahead" | "lookbehind" | "negativeLookbehind" | "backreference" | "namedBackreference" | "namedGroup" | "capturingGroup" | "nonCapturingGroup" | "modifierGroup" | "modifierGroupWithRemoval" | "characterClass" | "nestedCharacterClass" | "classIntersection" | "classSubtraction" | "disjunction" | "quantifier" | "unicodePropertyEscape" | "characterClassEscape" | "controlEscape" | "controlLetterEscape" | "hexEscapeSequence" | "unicodeEscapeSequence" | "otherEscape";
|
|
1
2
|
export interface DynamicPath {
|
|
2
3
|
originalCaptureScan: RegExp;
|
|
3
4
|
preScan: RegExp;
|
|
4
5
|
expand: (capture: RegExpExecArray) => string;
|
|
5
6
|
}
|
|
6
|
-
export type CompiledPartial = {
|
|
7
|
+
export type CompiledPartial = ({
|
|
7
8
|
kind: "static";
|
|
8
9
|
regex: RegExp;
|
|
9
10
|
} | {
|
|
10
11
|
kind: "dynamic";
|
|
11
12
|
dynamic: DynamicPath;
|
|
13
|
+
}) & {
|
|
14
|
+
features: Set<RegexFeature>;
|
|
12
15
|
};
|
|
13
16
|
export declare const compilePartial: (regex: RegExp) => CompiledPartial;
|
package/lib/compilePartial.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import escapeAtom from "./escapeAtom.js";
|
|
2
2
|
const OCCURRENCES_REGEX = /\{\d+,?\d*\}/y;
|
|
3
3
|
const NOT_NUMBERS_REGEX = /\D/g;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const MAYBE_HAS_BACKREFERENCE_REGEX = /\\[1-9]|\\k</;
|
|
5
|
+
const DISJUNCTION_TO_END_OF_INPUT = "|$(?![\\s\\S]))";
|
|
6
6
|
const isBackreference = (part) => typeof part !== "string";
|
|
7
7
|
const isNumericBackreference = (part) => isBackreference(part) && typeof part.ref === "number";
|
|
8
8
|
function walk(regex) {
|
|
@@ -10,6 +10,7 @@ function walk(regex) {
|
|
|
10
10
|
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
11
11
|
let i = 0;
|
|
12
12
|
let groupCount = 0;
|
|
13
|
+
const features = new Set();
|
|
13
14
|
function extractSlice(length) {
|
|
14
15
|
return source.slice(i, (i += length));
|
|
15
16
|
}
|
|
@@ -17,7 +18,7 @@ function walk(regex) {
|
|
|
17
18
|
var _a;
|
|
18
19
|
const result = [];
|
|
19
20
|
function appendOptional(length) {
|
|
20
|
-
result.push("(?:" + extractSlice(length) +
|
|
21
|
+
result.push("(?:" + extractSlice(length) + DISJUNCTION_TO_END_OF_INPUT);
|
|
21
22
|
}
|
|
22
23
|
function appendRaw(length) {
|
|
23
24
|
result.push(extractSlice(length));
|
|
@@ -33,6 +34,7 @@ function walk(regex) {
|
|
|
33
34
|
case "\\":
|
|
34
35
|
switch (source[i + 1]) {
|
|
35
36
|
case "c":
|
|
37
|
+
features.add("controlLetterEscape");
|
|
36
38
|
appendOptional(3);
|
|
37
39
|
break;
|
|
38
40
|
case "k": {
|
|
@@ -41,6 +43,7 @@ function walk(regex) {
|
|
|
41
43
|
appendOptional(2);
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
46
|
+
features.add("namedBackreference");
|
|
44
47
|
const start = i;
|
|
45
48
|
const ref = source.slice(i + 3, referenceEnd);
|
|
46
49
|
i = referenceEnd + 1;
|
|
@@ -49,6 +52,7 @@ function walk(regex) {
|
|
|
49
52
|
break;
|
|
50
53
|
}
|
|
51
54
|
case "u":
|
|
55
|
+
features.add("unicodeEscapeSequence");
|
|
52
56
|
if (isUnicode && source[i + 2] === "{") {
|
|
53
57
|
appendOptional(source.indexOf("}", i) - i + 1);
|
|
54
58
|
}
|
|
@@ -59,6 +63,7 @@ function walk(regex) {
|
|
|
59
63
|
case "p":
|
|
60
64
|
case "P":
|
|
61
65
|
if (isUnicode) {
|
|
66
|
+
features.add("unicodePropertyEscape");
|
|
62
67
|
appendOptional(source.indexOf("}", i) - i + 1);
|
|
63
68
|
}
|
|
64
69
|
else {
|
|
@@ -66,8 +71,25 @@ function walk(regex) {
|
|
|
66
71
|
}
|
|
67
72
|
break;
|
|
68
73
|
case "x":
|
|
74
|
+
features.add("hexEscapeSequence");
|
|
69
75
|
appendOptional(4);
|
|
70
76
|
break;
|
|
77
|
+
case "b":
|
|
78
|
+
features.add("wordBoundary");
|
|
79
|
+
appendOptional(2);
|
|
80
|
+
break;
|
|
81
|
+
case "B":
|
|
82
|
+
features.add("nonWordBoundary");
|
|
83
|
+
appendOptional(2);
|
|
84
|
+
break;
|
|
85
|
+
case "f":
|
|
86
|
+
case "n":
|
|
87
|
+
case "r":
|
|
88
|
+
case "t":
|
|
89
|
+
case "v":
|
|
90
|
+
features.add("controlEscape");
|
|
91
|
+
appendOptional(2);
|
|
92
|
+
break;
|
|
71
93
|
case "1":
|
|
72
94
|
case "2":
|
|
73
95
|
case "3":
|
|
@@ -77,6 +99,7 @@ function walk(regex) {
|
|
|
77
99
|
case "7":
|
|
78
100
|
case "8":
|
|
79
101
|
case "9": {
|
|
102
|
+
features.add("backreference");
|
|
80
103
|
NOT_NUMBERS_REGEX.lastIndex = i + 1;
|
|
81
104
|
const nextNonDigit = NOT_NUMBERS_REGEX.exec(source);
|
|
82
105
|
const start = i;
|
|
@@ -86,43 +109,80 @@ function walk(regex) {
|
|
|
86
109
|
result.push({ ref, start, end });
|
|
87
110
|
break;
|
|
88
111
|
}
|
|
112
|
+
case "d":
|
|
113
|
+
case "D":
|
|
114
|
+
case "w":
|
|
115
|
+
case "W":
|
|
116
|
+
case "s":
|
|
117
|
+
case "S":
|
|
118
|
+
features.add("characterClassEscape");
|
|
119
|
+
appendOptional(2);
|
|
120
|
+
break;
|
|
89
121
|
default:
|
|
122
|
+
features.add("otherEscape");
|
|
90
123
|
appendOptional(2);
|
|
91
124
|
break;
|
|
92
125
|
}
|
|
93
126
|
break;
|
|
94
127
|
case "[": {
|
|
95
|
-
|
|
128
|
+
features.add("characterClass");
|
|
129
|
+
let depth = 1, j = i + 1, previousSetOperatorCharacter;
|
|
96
130
|
while (depth) {
|
|
97
|
-
|
|
131
|
+
const character = source[j];
|
|
132
|
+
switch (character) {
|
|
98
133
|
case "\\":
|
|
99
134
|
j += 2;
|
|
135
|
+
previousSetOperatorCharacter = undefined;
|
|
100
136
|
continue;
|
|
101
137
|
case "[":
|
|
102
|
-
if (regex.unicodeSets)
|
|
138
|
+
if (regex.unicodeSets) {
|
|
139
|
+
features.add("nestedCharacterClass");
|
|
103
140
|
depth++;
|
|
141
|
+
}
|
|
104
142
|
break;
|
|
105
143
|
case "]":
|
|
106
144
|
depth--;
|
|
107
145
|
break;
|
|
146
|
+
case "&":
|
|
147
|
+
if (regex.unicodeSets && previousSetOperatorCharacter === "&") {
|
|
148
|
+
features.add("classIntersection");
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case "-":
|
|
152
|
+
if (regex.unicodeSets && previousSetOperatorCharacter === "-") {
|
|
153
|
+
features.add("classSubtraction");
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
108
156
|
}
|
|
157
|
+
previousSetOperatorCharacter = character;
|
|
109
158
|
j++;
|
|
110
159
|
}
|
|
111
160
|
appendOptional(j - i);
|
|
112
161
|
break;
|
|
113
162
|
}
|
|
114
|
-
case "|":
|
|
115
163
|
case "^":
|
|
164
|
+
features.add("startAnchor");
|
|
165
|
+
appendRaw(1);
|
|
166
|
+
break;
|
|
167
|
+
case "$":
|
|
168
|
+
features.add("endAnchor");
|
|
169
|
+
appendRaw(1);
|
|
170
|
+
break;
|
|
171
|
+
case "|":
|
|
172
|
+
features.add("disjunction");
|
|
173
|
+
appendRaw(1);
|
|
174
|
+
break;
|
|
116
175
|
case "*":
|
|
117
176
|
case "+":
|
|
118
177
|
case "?":
|
|
119
|
-
|
|
178
|
+
features.add("quantifier");
|
|
120
179
|
appendRaw(1);
|
|
121
180
|
break;
|
|
122
181
|
case "{": {
|
|
123
182
|
OCCURRENCES_REGEX.lastIndex = i;
|
|
124
183
|
const regExpExecArray = OCCURRENCES_REGEX.exec(source);
|
|
125
184
|
if (regExpExecArray) {
|
|
185
|
+
features.add("quantifier");
|
|
126
186
|
appendRaw(regExpExecArray[0].length);
|
|
127
187
|
}
|
|
128
188
|
else {
|
|
@@ -134,11 +194,13 @@ function walk(regex) {
|
|
|
134
194
|
if (source[i + 1] == "?") {
|
|
135
195
|
switch (source[i + 2]) {
|
|
136
196
|
case ":":
|
|
197
|
+
features.add("nonCapturingGroup");
|
|
137
198
|
result.push("(?:");
|
|
138
199
|
i += 3;
|
|
139
|
-
result.push(...process(),
|
|
200
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
140
201
|
break;
|
|
141
202
|
case "=":
|
|
203
|
+
features.add("lookahead");
|
|
142
204
|
result.push("(?=");
|
|
143
205
|
i += 3;
|
|
144
206
|
result.push(...process(), ")");
|
|
@@ -148,46 +210,59 @@ function walk(regex) {
|
|
|
148
210
|
case "s":
|
|
149
211
|
case "m": {
|
|
150
212
|
const flagsStart = i + 2, colonIndex = source.indexOf(":", flagsStart);
|
|
151
|
-
|
|
213
|
+
const modifiers = source.slice(flagsStart, colonIndex);
|
|
214
|
+
features.add(modifiers.includes("-")
|
|
215
|
+
? "modifierGroupWithRemoval"
|
|
216
|
+
: "modifierGroup");
|
|
217
|
+
result.push("(?" + modifiers + ":");
|
|
152
218
|
i = colonIndex + 1;
|
|
153
219
|
result.push(...process(), ")");
|
|
154
220
|
break;
|
|
155
221
|
}
|
|
156
222
|
case "!":
|
|
223
|
+
features.add("negativeLookahead");
|
|
157
224
|
appendRawGroup(3);
|
|
158
225
|
break;
|
|
159
226
|
case "<":
|
|
160
227
|
switch (source[i + 3]) {
|
|
161
228
|
case "=":
|
|
229
|
+
features.add("lookbehind");
|
|
230
|
+
appendRawGroup(4);
|
|
231
|
+
break;
|
|
162
232
|
case "!":
|
|
233
|
+
features.add("negativeLookbehind");
|
|
163
234
|
appendRawGroup(4);
|
|
164
235
|
break;
|
|
165
236
|
default:
|
|
237
|
+
features.add("namedGroup");
|
|
238
|
+
features.add("capturingGroup");
|
|
166
239
|
++groupCount;
|
|
167
240
|
appendRaw(source.indexOf(">", i) - i + 1);
|
|
168
|
-
result.push(...process(),
|
|
241
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
169
242
|
break;
|
|
170
243
|
}
|
|
171
244
|
break;
|
|
172
245
|
}
|
|
173
246
|
}
|
|
174
247
|
else {
|
|
248
|
+
features.add("capturingGroup");
|
|
175
249
|
++groupCount;
|
|
176
250
|
appendRaw(1);
|
|
177
|
-
result.push(...process(),
|
|
251
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
178
252
|
}
|
|
179
253
|
break;
|
|
180
254
|
case ")":
|
|
181
255
|
++i;
|
|
182
256
|
return result;
|
|
183
257
|
default:
|
|
258
|
+
features.add("patternCharacter");
|
|
184
259
|
appendOptional(isUnicode && ((_a = source.codePointAt(i)) !== null && _a !== void 0 ? _a : 0) > 0xffff ? 2 : 1);
|
|
185
260
|
break;
|
|
186
261
|
}
|
|
187
262
|
}
|
|
188
263
|
return result;
|
|
189
264
|
}
|
|
190
|
-
return { parts: process(), groupCount };
|
|
265
|
+
return { parts: process(), groupCount, features };
|
|
191
266
|
}
|
|
192
267
|
function render(parts, renderBackref) {
|
|
193
268
|
return parts
|
|
@@ -201,7 +276,7 @@ function backrefToken(backref) {
|
|
|
201
276
|
}
|
|
202
277
|
function reclassifyOctalEscapes(parts, source, groupCount) {
|
|
203
278
|
return parts.map((part) => isNumericBackreference(part) && part.ref > groupCount
|
|
204
|
-
? "(?:" + source.slice(part.start, part.end) +
|
|
279
|
+
? "(?:" + source.slice(part.start, part.end) + DISJUNCTION_TO_END_OF_INPUT
|
|
205
280
|
: part);
|
|
206
281
|
}
|
|
207
282
|
function spliceOriginalSource(source, backrefs) {
|
|
@@ -215,30 +290,33 @@ function spliceOriginalSource(source, backrefs) {
|
|
|
215
290
|
}
|
|
216
291
|
function expandCaptured(value, isUnicode) {
|
|
217
292
|
return (isUnicode ? Array.from(value) : value.split(""))
|
|
218
|
-
.map((atom) => "(?:" + escapeAtom(atom) +
|
|
293
|
+
.map((atom) => "(?:" + escapeAtom(atom) + DISJUNCTION_TO_END_OF_INPUT)
|
|
219
294
|
.join("");
|
|
220
295
|
}
|
|
221
296
|
export const compilePartial = (regex) => {
|
|
222
|
-
const { parts, groupCount } = walk(regex);
|
|
223
|
-
if (!
|
|
297
|
+
const { parts, groupCount, features } = walk(regex);
|
|
298
|
+
if (!MAYBE_HAS_BACKREFERENCE_REGEX.test(regex.source)) {
|
|
224
299
|
return {
|
|
225
300
|
kind: "static",
|
|
226
|
-
regex: new RegExp(render(parts, backrefToken), regex.flags)
|
|
301
|
+
regex: new RegExp(render(parts, backrefToken), regex.flags),
|
|
302
|
+
features
|
|
227
303
|
};
|
|
228
304
|
}
|
|
229
305
|
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
230
|
-
const
|
|
306
|
+
const sanitisedParts = isUnicode
|
|
231
307
|
? parts
|
|
232
308
|
: reclassifyOctalEscapes(parts, regex.source, groupCount);
|
|
233
|
-
const backreferences =
|
|
309
|
+
const backreferences = sanitisedParts.filter(isBackreference);
|
|
234
310
|
if (backreferences.length === 0) {
|
|
235
311
|
return {
|
|
236
312
|
kind: "static",
|
|
237
|
-
regex: new RegExp(render(
|
|
313
|
+
regex: new RegExp(render(sanitisedParts, backrefToken), regex.flags),
|
|
314
|
+
features
|
|
238
315
|
};
|
|
239
316
|
}
|
|
240
317
|
return {
|
|
241
318
|
kind: "dynamic",
|
|
319
|
+
features,
|
|
242
320
|
dynamic: {
|
|
243
321
|
originalCaptureScan: new RegExp(spliceOriginalSource(regex.source, backreferences), regex.flags),
|
|
244
322
|
preScan: new RegExp(render(parts, () => "(?:[\\s\\S]*?)"), regex.flags),
|
|
@@ -248,7 +326,7 @@ export const compilePartial = (regex) => {
|
|
|
248
326
|
? capture[backref.ref]
|
|
249
327
|
: (_a = capture.groups) === null || _a === void 0 ? void 0 : _a[backref.ref];
|
|
250
328
|
return captured === undefined
|
|
251
|
-
? "(?:" + backrefToken(backref) +
|
|
329
|
+
? "(?:" + backrefToken(backref) + DISJUNCTION_TO_END_OF_INPUT
|
|
252
330
|
: expandCaptured(captured, isUnicode);
|
|
253
331
|
})
|
|
254
332
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RegexFeature } from "./compilePartial.ts";
|
|
2
|
+
export type { RegexFeature };
|
|
1
3
|
/**
|
|
2
4
|
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
3
5
|
*
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
*/
|
|
27
29
|
declare class PartialMatchRegExp extends RegExp {
|
|
28
30
|
#private;
|
|
31
|
+
readonly features: ReadonlySet<RegexFeature>;
|
|
29
32
|
constructor(pattern: RegExp | string, flags?: string);
|
|
30
33
|
exec(input: string): RegExpExecArray | null;
|
|
31
34
|
}
|
|
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var
|
|
12
|
+
var _PartialMatchRegExp_instances, _PartialMatchRegExp_compiledPartial, _PartialMatchRegExp_execDynamic;
|
|
13
13
|
import { compilePartial } from "./compilePartial.js";
|
|
14
14
|
/**
|
|
15
15
|
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
@@ -39,47 +39,56 @@ import { compilePartial } from "./compilePartial.js";
|
|
|
39
39
|
*/
|
|
40
40
|
class PartialMatchRegExp extends RegExp {
|
|
41
41
|
constructor(pattern, flags) {
|
|
42
|
-
var _a, _b;
|
|
43
42
|
super(pattern, flags);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
? [compiled.dynamic, null]
|
|
49
|
-
: [null, compiled.regex];
|
|
43
|
+
_PartialMatchRegExp_instances.add(this);
|
|
44
|
+
_PartialMatchRegExp_compiledPartial.set(this, void 0);
|
|
45
|
+
__classPrivateFieldSet(this, _PartialMatchRegExp_compiledPartial, compilePartial(this), "f");
|
|
46
|
+
this.features = __classPrivateFieldGet(this, _PartialMatchRegExp_compiledPartial, "f").features;
|
|
50
47
|
}
|
|
51
48
|
exec(input) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const start = this.lastIndex;
|
|
60
|
-
const originalMatch = super.exec(input);
|
|
61
|
-
if (originalMatch)
|
|
62
|
-
return originalMatch;
|
|
63
|
-
const { originalCaptureScan, preScan, expand } = __classPrivateFieldGet(this, _PartialMatchRegExp_dynamic, "f");
|
|
64
|
-
originalCaptureScan.lastIndex = start;
|
|
65
|
-
let capture = originalCaptureScan.exec(input);
|
|
66
|
-
if (capture === null) {
|
|
67
|
-
preScan.lastIndex = start;
|
|
68
|
-
capture = preScan.exec(input);
|
|
69
|
-
}
|
|
70
|
-
if (capture === null)
|
|
71
|
-
return null;
|
|
72
|
-
const expanded = new RegExp(expand(capture), this.flags);
|
|
73
|
-
expanded.lastIndex = start;
|
|
74
|
-
const match = expanded.exec(input);
|
|
75
|
-
if (match === null)
|
|
76
|
-
return null;
|
|
77
|
-
preferLongerCaptures(match, capture);
|
|
78
|
-
this.lastIndex = expanded.lastIndex;
|
|
49
|
+
const compiled = __classPrivateFieldGet(this, _PartialMatchRegExp_compiledPartial, "f");
|
|
50
|
+
if (compiled.kind === "dynamic")
|
|
51
|
+
return __classPrivateFieldGet(this, _PartialMatchRegExp_instances, "m", _PartialMatchRegExp_execDynamic).call(this, compiled.dynamic, input);
|
|
52
|
+
const { regex } = compiled;
|
|
53
|
+
const match = execFrom(regex, input, this.lastIndex);
|
|
54
|
+
this.lastIndex = regex.lastIndex;
|
|
79
55
|
return match;
|
|
80
56
|
}
|
|
81
57
|
}
|
|
82
|
-
|
|
58
|
+
_PartialMatchRegExp_compiledPartial = new WeakMap(), _PartialMatchRegExp_instances = new WeakSet(), _PartialMatchRegExp_execDynamic = function _PartialMatchRegExp_execDynamic(dynamic, input) {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
const { originalCaptureScan, preScan, expand } = dynamic;
|
|
61
|
+
const honoursLastIndex = this.global || this.sticky;
|
|
62
|
+
const start = honoursLastIndex ? this.lastIndex : 0;
|
|
63
|
+
const originalMatch = super.exec(input);
|
|
64
|
+
if (isAtOrBefore(originalMatch, start))
|
|
65
|
+
return originalMatch;
|
|
66
|
+
let preScanMatch = null;
|
|
67
|
+
if (originalMatch) {
|
|
68
|
+
preScanMatch = execFrom(preScan, input, start);
|
|
69
|
+
const noEarlierPartialPossible = preScanMatch === null || preScanMatch.index >= originalMatch.index;
|
|
70
|
+
if (noEarlierPartialPossible)
|
|
71
|
+
return originalMatch;
|
|
72
|
+
}
|
|
73
|
+
const capture = (_b = (_a = execFrom(originalCaptureScan, input, start)) !== null && _a !== void 0 ? _a : preScanMatch) !== null && _b !== void 0 ? _b : execFrom(preScan, input, start);
|
|
74
|
+
if (capture === null)
|
|
75
|
+
return originalMatch;
|
|
76
|
+
const expanded = new RegExp(expand(capture), this.flags);
|
|
77
|
+
const match = execFrom(expanded, input, start);
|
|
78
|
+
if (match === null || isAtOrBefore(originalMatch, match.index))
|
|
79
|
+
return originalMatch;
|
|
80
|
+
preferLongerCaptures(match, capture);
|
|
81
|
+
if (honoursLastIndex)
|
|
82
|
+
this.lastIndex = expanded.lastIndex;
|
|
83
|
+
return match;
|
|
84
|
+
};
|
|
85
|
+
function execFrom(regex, input, start) {
|
|
86
|
+
regex.lastIndex = start;
|
|
87
|
+
return regex.exec(input);
|
|
88
|
+
}
|
|
89
|
+
function isAtOrBefore(match, index) {
|
|
90
|
+
return match !== null && match.index <= index;
|
|
91
|
+
}
|
|
83
92
|
function pickLonger(scanned, matched) {
|
|
84
93
|
return scanned !== undefined &&
|
|
85
94
|
matched !== undefined &&
|