regex-partial-match 1.1.1 → 1.2.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/README.md +5 -2
- package/lib/compilePartial.d.ts +20 -10
- package/lib/compilePartial.js +140 -77
- package/lib/partialMatchRegExp.d.ts +19 -2
- package/lib/partialMatchRegExp.js +48 -44
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -372,13 +372,14 @@ This is useful for consumers building on top of `PartialMatchRegExp` who need to
|
|
|
372
372
|
|
|
373
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
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
|
+
- **Deciding at construction time whether a pattern needs a careful path.** A capture nested inside a lookaround is decided by the assertion rather than by the consumed text, so its value can vary with how far the input has been seen. `features.has("lookaroundCapture")` isolates exactly those patterns, where `features.has("lookahead") && features.has("capturingGroup")` would also catch the ordinary `/(\w+)(?= END)/`.
|
|
375
376
|
|
|
376
377
|
```javascript
|
|
377
378
|
import PartialMatchRegExp from "regex-partial-match";
|
|
378
379
|
|
|
379
380
|
const partial = new PartialMatchRegExp(/^[a-z]+(?<domain>\.[a-z]+)\1/);
|
|
380
381
|
|
|
381
|
-
partial.features; // Set { "startAnchor", "
|
|
382
|
+
partial.features; // Set { "startAnchor", "backreference", "namedGroup", "capturingGroup", "characterClass", "quantifier", "otherEscape" }
|
|
382
383
|
partial.features.has("backreference"); // true
|
|
383
384
|
```
|
|
384
385
|
|
|
@@ -399,6 +400,7 @@ partial.features.has("backreference"); // true
|
|
|
399
400
|
| `namedBackreference` | `\k<name>` | |
|
|
400
401
|
| `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
|
|
401
402
|
| `capturingGroup` | `(...)`, including named groups | |
|
|
403
|
+
| `lookaroundCapture` | A capturing group inside any lookaround | Nesting, not a construct — always accompanied by `capturingGroup` |
|
|
402
404
|
| `nonCapturingGroup` | `(?:...)` | |
|
|
403
405
|
| `modifierGroup` | `(?ims:...)` | |
|
|
404
406
|
| `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
|
|
@@ -416,10 +418,11 @@ partial.features.has("backreference"); // true
|
|
|
416
418
|
| `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
|
|
417
419
|
| `otherEscape` | Any other `\X`, e.g. `\.`, `\0` | |
|
|
418
420
|
|
|
419
|
-
|
|
421
|
+
Three things worth knowing about how these tags line up with the grammar:
|
|
420
422
|
|
|
421
423
|
- **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
424
|
- **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.
|
|
425
|
+
- **`lookaroundCapture` records nesting, not a construct.** Every other tag names something the source contains; this one names where something sits — a capturing group appearing lexically inside `(?=...)`, `(?!...)`, `(?<=...)` or `(?<!...)`, at any depth of nested groups. It is the one relationship between constructs the flattened set cannot otherwise express: `/(\w+)(?= END)/` and `/a(?=(?:b(?:x|(c))d|b))/` both report `lookahead` and `capturingGroup`, but only the second has a capture whose value the assertion decides.
|
|
423
426
|
|
|
424
427
|
## 📜 License
|
|
425
428
|
|
package/lib/compilePartial.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
declare const REGEX_FEATURES: readonly ["patternCharacter", "startAnchor", "endAnchor", "wordBoundary", "nonWordBoundary", "lookahead", "negativeLookahead", "lookbehind", "negativeLookbehind", "backreference", "namedBackreference", "namedGroup", "capturingGroup", "lookaroundCapture", "nonCapturingGroup", "modifierGroup", "modifierGroupWithRemoval", "characterClass", "nestedCharacterClass", "classIntersection", "classSubtraction", "disjunction", "quantifier", "unicodePropertyEscape", "characterClassEscape", "controlEscape", "controlLetterEscape", "hexEscapeSequence", "unicodeEscapeSequence", "otherEscape"];
|
|
2
|
+
export type RegexFeature = (typeof REGEX_FEATURES)[number];
|
|
2
3
|
export interface DynamicPath {
|
|
3
4
|
originalCaptureScan: RegExp;
|
|
4
5
|
preScan: RegExp;
|
|
5
6
|
expand: (capture: RegExpExecArray) => string;
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
declare abstract class Compiled {
|
|
9
|
+
private readonly _featureMask;
|
|
10
|
+
private _features?;
|
|
11
|
+
constructor(_featureMask: number);
|
|
12
|
+
get features(): ReadonlySet<RegexFeature>;
|
|
13
|
+
}
|
|
14
|
+
declare class CompiledStatic extends Compiled {
|
|
15
|
+
readonly regex: RegExp;
|
|
16
|
+
readonly kind = "static";
|
|
17
|
+
constructor(regex: RegExp, featureMask: number);
|
|
18
|
+
}
|
|
19
|
+
declare class CompiledDynamic extends Compiled {
|
|
20
|
+
readonly dynamic: DynamicPath;
|
|
21
|
+
readonly kind = "dynamic";
|
|
22
|
+
constructor(dynamic: DynamicPath, featureMask: number);
|
|
23
|
+
}
|
|
24
|
+
export type CompiledPartial = CompiledStatic | CompiledDynamic;
|
|
16
25
|
export declare const compilePartial: (regex: RegExp) => CompiledPartial;
|
|
26
|
+
export {};
|
package/lib/compilePartial.js
CHANGED
|
@@ -3,18 +3,63 @@ const OCCURRENCES_REGEX = /\{\d+,?\d*\}/y;
|
|
|
3
3
|
const NOT_NUMBERS_REGEX = /\D/g;
|
|
4
4
|
const MAYBE_HAS_BACKREFERENCE_REGEX = /\\[1-9]|\\k</;
|
|
5
5
|
const DISJUNCTION_TO_END_OF_INPUT = "|$(?![\\s\\S]))";
|
|
6
|
+
const ANY_CAPTURED_TEXT = "(?:[\\s\\S]*?)";
|
|
6
7
|
const isBackreference = (part) => typeof part !== "string";
|
|
7
8
|
const isNumericBackreference = (part) => isBackreference(part) && typeof part.ref === "number";
|
|
9
|
+
const REGEX_FEATURES = [
|
|
10
|
+
"patternCharacter",
|
|
11
|
+
"startAnchor",
|
|
12
|
+
"endAnchor",
|
|
13
|
+
"wordBoundary",
|
|
14
|
+
"nonWordBoundary",
|
|
15
|
+
"lookahead",
|
|
16
|
+
"negativeLookahead",
|
|
17
|
+
"lookbehind",
|
|
18
|
+
"negativeLookbehind",
|
|
19
|
+
"backreference",
|
|
20
|
+
"namedBackreference",
|
|
21
|
+
"namedGroup",
|
|
22
|
+
"capturingGroup",
|
|
23
|
+
"lookaroundCapture",
|
|
24
|
+
"nonCapturingGroup",
|
|
25
|
+
"modifierGroup",
|
|
26
|
+
"modifierGroupWithRemoval",
|
|
27
|
+
"characterClass",
|
|
28
|
+
"nestedCharacterClass",
|
|
29
|
+
"classIntersection",
|
|
30
|
+
"classSubtraction",
|
|
31
|
+
"disjunction",
|
|
32
|
+
"quantifier",
|
|
33
|
+
"unicodePropertyEscape",
|
|
34
|
+
"characterClassEscape",
|
|
35
|
+
"controlEscape",
|
|
36
|
+
"controlLetterEscape",
|
|
37
|
+
"hexEscapeSequence",
|
|
38
|
+
"unicodeEscapeSequence",
|
|
39
|
+
"otherEscape"
|
|
40
|
+
];
|
|
41
|
+
const FEATURE_BIT = {};
|
|
42
|
+
for (let index = 0; index < REGEX_FEATURES.length; index++) {
|
|
43
|
+
FEATURE_BIT[REGEX_FEATURES[index]] = 1 << index;
|
|
44
|
+
}
|
|
45
|
+
function featureSet(mask) {
|
|
46
|
+
const features = new Set();
|
|
47
|
+
for (let index = 0; index < REGEX_FEATURES.length; index++) {
|
|
48
|
+
if (mask & (1 << index))
|
|
49
|
+
features.add(REGEX_FEATURES[index]);
|
|
50
|
+
}
|
|
51
|
+
return features;
|
|
52
|
+
}
|
|
8
53
|
function walk(regex) {
|
|
9
54
|
const source = regex.source;
|
|
10
55
|
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
11
56
|
let i = 0;
|
|
12
57
|
let groupCount = 0;
|
|
13
|
-
|
|
58
|
+
let featureMask = 0;
|
|
14
59
|
function extractSlice(length) {
|
|
15
60
|
return source.slice(i, (i += length));
|
|
16
61
|
}
|
|
17
|
-
function process() {
|
|
62
|
+
function process(withinLookaround) {
|
|
18
63
|
var _a;
|
|
19
64
|
const result = [];
|
|
20
65
|
function appendOptional(length) {
|
|
@@ -23,10 +68,10 @@ function walk(regex) {
|
|
|
23
68
|
function appendRaw(length) {
|
|
24
69
|
result.push(extractSlice(length));
|
|
25
70
|
}
|
|
26
|
-
function
|
|
71
|
+
function appendRawLookaround(prefixLength) {
|
|
27
72
|
const start = i;
|
|
28
73
|
i += prefixLength;
|
|
29
|
-
process();
|
|
74
|
+
process(true);
|
|
30
75
|
result.push(source.slice(start, i));
|
|
31
76
|
}
|
|
32
77
|
while (i < source.length) {
|
|
@@ -34,7 +79,7 @@ function walk(regex) {
|
|
|
34
79
|
case "\\":
|
|
35
80
|
switch (source[i + 1]) {
|
|
36
81
|
case "c":
|
|
37
|
-
|
|
82
|
+
featureMask |= FEATURE_BIT.controlLetterEscape;
|
|
38
83
|
appendOptional(3);
|
|
39
84
|
break;
|
|
40
85
|
case "k": {
|
|
@@ -43,7 +88,7 @@ function walk(regex) {
|
|
|
43
88
|
appendOptional(2);
|
|
44
89
|
}
|
|
45
90
|
else {
|
|
46
|
-
|
|
91
|
+
featureMask |= FEATURE_BIT.namedBackreference;
|
|
47
92
|
const start = i;
|
|
48
93
|
const ref = source.slice(i + 3, referenceEnd);
|
|
49
94
|
i = referenceEnd + 1;
|
|
@@ -52,7 +97,7 @@ function walk(regex) {
|
|
|
52
97
|
break;
|
|
53
98
|
}
|
|
54
99
|
case "u":
|
|
55
|
-
|
|
100
|
+
featureMask |= FEATURE_BIT.unicodeEscapeSequence;
|
|
56
101
|
if (isUnicode && source[i + 2] === "{") {
|
|
57
102
|
appendOptional(source.indexOf("}", i) - i + 1);
|
|
58
103
|
}
|
|
@@ -63,7 +108,7 @@ function walk(regex) {
|
|
|
63
108
|
case "p":
|
|
64
109
|
case "P":
|
|
65
110
|
if (isUnicode) {
|
|
66
|
-
|
|
111
|
+
featureMask |= FEATURE_BIT.unicodePropertyEscape;
|
|
67
112
|
appendOptional(source.indexOf("}", i) - i + 1);
|
|
68
113
|
}
|
|
69
114
|
else {
|
|
@@ -71,15 +116,15 @@ function walk(regex) {
|
|
|
71
116
|
}
|
|
72
117
|
break;
|
|
73
118
|
case "x":
|
|
74
|
-
|
|
119
|
+
featureMask |= FEATURE_BIT.hexEscapeSequence;
|
|
75
120
|
appendOptional(4);
|
|
76
121
|
break;
|
|
77
122
|
case "b":
|
|
78
|
-
|
|
123
|
+
featureMask |= FEATURE_BIT.wordBoundary;
|
|
79
124
|
appendOptional(2);
|
|
80
125
|
break;
|
|
81
126
|
case "B":
|
|
82
|
-
|
|
127
|
+
featureMask |= FEATURE_BIT.nonWordBoundary;
|
|
83
128
|
appendOptional(2);
|
|
84
129
|
break;
|
|
85
130
|
case "f":
|
|
@@ -87,7 +132,7 @@ function walk(regex) {
|
|
|
87
132
|
case "r":
|
|
88
133
|
case "t":
|
|
89
134
|
case "v":
|
|
90
|
-
|
|
135
|
+
featureMask |= FEATURE_BIT.controlEscape;
|
|
91
136
|
appendOptional(2);
|
|
92
137
|
break;
|
|
93
138
|
case "1":
|
|
@@ -99,7 +144,7 @@ function walk(regex) {
|
|
|
99
144
|
case "7":
|
|
100
145
|
case "8":
|
|
101
146
|
case "9": {
|
|
102
|
-
|
|
147
|
+
featureMask |= FEATURE_BIT.backreference;
|
|
103
148
|
NOT_NUMBERS_REGEX.lastIndex = i + 1;
|
|
104
149
|
const nextNonDigit = NOT_NUMBERS_REGEX.exec(source);
|
|
105
150
|
const start = i;
|
|
@@ -115,17 +160,17 @@ function walk(regex) {
|
|
|
115
160
|
case "W":
|
|
116
161
|
case "s":
|
|
117
162
|
case "S":
|
|
118
|
-
|
|
163
|
+
featureMask |= FEATURE_BIT.characterClassEscape;
|
|
119
164
|
appendOptional(2);
|
|
120
165
|
break;
|
|
121
166
|
default:
|
|
122
|
-
|
|
167
|
+
featureMask |= FEATURE_BIT.otherEscape;
|
|
123
168
|
appendOptional(2);
|
|
124
169
|
break;
|
|
125
170
|
}
|
|
126
171
|
break;
|
|
127
172
|
case "[": {
|
|
128
|
-
|
|
173
|
+
featureMask |= FEATURE_BIT.characterClass;
|
|
129
174
|
let depth = 1, j = i + 1, previousSetOperatorCharacter;
|
|
130
175
|
while (depth) {
|
|
131
176
|
const character = source[j];
|
|
@@ -136,7 +181,7 @@ function walk(regex) {
|
|
|
136
181
|
continue;
|
|
137
182
|
case "[":
|
|
138
183
|
if (regex.unicodeSets) {
|
|
139
|
-
|
|
184
|
+
featureMask |= FEATURE_BIT.nestedCharacterClass;
|
|
140
185
|
depth++;
|
|
141
186
|
}
|
|
142
187
|
break;
|
|
@@ -145,12 +190,12 @@ function walk(regex) {
|
|
|
145
190
|
break;
|
|
146
191
|
case "&":
|
|
147
192
|
if (regex.unicodeSets && previousSetOperatorCharacter === "&") {
|
|
148
|
-
|
|
193
|
+
featureMask |= FEATURE_BIT.classIntersection;
|
|
149
194
|
}
|
|
150
195
|
break;
|
|
151
196
|
case "-":
|
|
152
197
|
if (regex.unicodeSets && previousSetOperatorCharacter === "-") {
|
|
153
|
-
|
|
198
|
+
featureMask |= FEATURE_BIT.classSubtraction;
|
|
154
199
|
}
|
|
155
200
|
break;
|
|
156
201
|
}
|
|
@@ -161,28 +206,28 @@ function walk(regex) {
|
|
|
161
206
|
break;
|
|
162
207
|
}
|
|
163
208
|
case "^":
|
|
164
|
-
|
|
209
|
+
featureMask |= FEATURE_BIT.startAnchor;
|
|
165
210
|
appendRaw(1);
|
|
166
211
|
break;
|
|
167
212
|
case "$":
|
|
168
|
-
|
|
213
|
+
featureMask |= FEATURE_BIT.endAnchor;
|
|
169
214
|
appendRaw(1);
|
|
170
215
|
break;
|
|
171
216
|
case "|":
|
|
172
|
-
|
|
217
|
+
featureMask |= FEATURE_BIT.disjunction;
|
|
173
218
|
appendRaw(1);
|
|
174
219
|
break;
|
|
175
220
|
case "*":
|
|
176
221
|
case "+":
|
|
177
222
|
case "?":
|
|
178
|
-
|
|
223
|
+
featureMask |= FEATURE_BIT.quantifier;
|
|
179
224
|
appendRaw(1);
|
|
180
225
|
break;
|
|
181
226
|
case "{": {
|
|
182
227
|
OCCURRENCES_REGEX.lastIndex = i;
|
|
183
228
|
const regExpExecArray = OCCURRENCES_REGEX.exec(source);
|
|
184
229
|
if (regExpExecArray) {
|
|
185
|
-
|
|
230
|
+
featureMask |= FEATURE_BIT.quantifier;
|
|
186
231
|
appendRaw(regExpExecArray[0].length);
|
|
187
232
|
}
|
|
188
233
|
else {
|
|
@@ -194,16 +239,16 @@ function walk(regex) {
|
|
|
194
239
|
if (source[i + 1] == "?") {
|
|
195
240
|
switch (source[i + 2]) {
|
|
196
241
|
case ":":
|
|
197
|
-
|
|
242
|
+
featureMask |= FEATURE_BIT.nonCapturingGroup;
|
|
198
243
|
result.push("(?:");
|
|
199
244
|
i += 3;
|
|
200
|
-
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
245
|
+
result.push(...process(withinLookaround), DISJUNCTION_TO_END_OF_INPUT);
|
|
201
246
|
break;
|
|
202
247
|
case "=":
|
|
203
|
-
|
|
248
|
+
featureMask |= FEATURE_BIT.lookahead;
|
|
204
249
|
result.push("(?=");
|
|
205
250
|
i += 3;
|
|
206
|
-
result.push(...process(), ")");
|
|
251
|
+
result.push(...process(true), ")");
|
|
207
252
|
break;
|
|
208
253
|
case "-":
|
|
209
254
|
case "i":
|
|
@@ -211,63 +256,69 @@ function walk(regex) {
|
|
|
211
256
|
case "m": {
|
|
212
257
|
const flagsStart = i + 2, colonIndex = source.indexOf(":", flagsStart);
|
|
213
258
|
const modifiers = source.slice(flagsStart, colonIndex);
|
|
214
|
-
|
|
215
|
-
?
|
|
216
|
-
:
|
|
259
|
+
featureMask |= modifiers.includes("-")
|
|
260
|
+
? FEATURE_BIT.modifierGroupWithRemoval
|
|
261
|
+
: FEATURE_BIT.modifierGroup;
|
|
217
262
|
result.push("(?" + modifiers + ":");
|
|
218
263
|
i = colonIndex + 1;
|
|
219
|
-
result.push(...process(), ")");
|
|
264
|
+
result.push(...process(withinLookaround), ")");
|
|
220
265
|
break;
|
|
221
266
|
}
|
|
222
267
|
case "!":
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
featureMask |= FEATURE_BIT.negativeLookahead;
|
|
269
|
+
appendRawLookaround(3);
|
|
225
270
|
break;
|
|
226
271
|
case "<":
|
|
227
272
|
switch (source[i + 3]) {
|
|
228
273
|
case "=":
|
|
229
|
-
|
|
230
|
-
|
|
274
|
+
featureMask |= FEATURE_BIT.lookbehind;
|
|
275
|
+
appendRawLookaround(4);
|
|
231
276
|
break;
|
|
232
277
|
case "!":
|
|
233
|
-
|
|
234
|
-
|
|
278
|
+
featureMask |= FEATURE_BIT.negativeLookbehind;
|
|
279
|
+
appendRawLookaround(4);
|
|
235
280
|
break;
|
|
236
281
|
default:
|
|
237
|
-
|
|
238
|
-
|
|
282
|
+
featureMask |= FEATURE_BIT.namedGroup;
|
|
283
|
+
featureMask |= FEATURE_BIT.capturingGroup;
|
|
284
|
+
if (withinLookaround)
|
|
285
|
+
featureMask |= FEATURE_BIT.lookaroundCapture;
|
|
239
286
|
++groupCount;
|
|
240
287
|
appendRaw(source.indexOf(">", i) - i + 1);
|
|
241
|
-
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
288
|
+
result.push(...process(withinLookaround), DISJUNCTION_TO_END_OF_INPUT);
|
|
242
289
|
break;
|
|
243
290
|
}
|
|
244
291
|
break;
|
|
245
292
|
}
|
|
246
293
|
}
|
|
247
294
|
else {
|
|
248
|
-
|
|
295
|
+
featureMask |= FEATURE_BIT.capturingGroup;
|
|
296
|
+
if (withinLookaround)
|
|
297
|
+
featureMask |= FEATURE_BIT.lookaroundCapture;
|
|
249
298
|
++groupCount;
|
|
250
299
|
appendRaw(1);
|
|
251
|
-
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
300
|
+
result.push(...process(withinLookaround), DISJUNCTION_TO_END_OF_INPUT);
|
|
252
301
|
}
|
|
253
302
|
break;
|
|
254
303
|
case ")":
|
|
255
304
|
++i;
|
|
256
305
|
return result;
|
|
257
306
|
default:
|
|
258
|
-
|
|
307
|
+
featureMask |= FEATURE_BIT.patternCharacter;
|
|
259
308
|
appendOptional(isUnicode && ((_a = source.codePointAt(i)) !== null && _a !== void 0 ? _a : 0) > 0xffff ? 2 : 1);
|
|
260
309
|
break;
|
|
261
310
|
}
|
|
262
311
|
}
|
|
263
312
|
return result;
|
|
264
313
|
}
|
|
265
|
-
return { parts: process(), groupCount,
|
|
314
|
+
return { parts: process(false), groupCount, featureMask };
|
|
266
315
|
}
|
|
267
316
|
function render(parts, renderBackref) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
317
|
+
let rendered = "";
|
|
318
|
+
for (const part of parts) {
|
|
319
|
+
rendered += isBackreference(part) ? renderBackref(part) : part;
|
|
320
|
+
}
|
|
321
|
+
return rendered;
|
|
271
322
|
}
|
|
272
323
|
function backrefToken(backref) {
|
|
273
324
|
return isNumericBackreference(backref)
|
|
@@ -283,7 +334,7 @@ function spliceOriginalSource(source, backrefs) {
|
|
|
283
334
|
let result = "";
|
|
284
335
|
let cursor = 0;
|
|
285
336
|
for (const { start, end } of backrefs) {
|
|
286
|
-
result += source.slice(cursor, start) +
|
|
337
|
+
result += source.slice(cursor, start) + ANY_CAPTURED_TEXT;
|
|
287
338
|
cursor = end;
|
|
288
339
|
}
|
|
289
340
|
return result + source.slice(cursor);
|
|
@@ -293,14 +344,34 @@ function expandCaptured(value, isUnicode) {
|
|
|
293
344
|
.map((atom) => "(?:" + escapeAtom(atom) + DISJUNCTION_TO_END_OF_INPUT)
|
|
294
345
|
.join("");
|
|
295
346
|
}
|
|
347
|
+
class Compiled {
|
|
348
|
+
constructor(_featureMask) {
|
|
349
|
+
this._featureMask = _featureMask;
|
|
350
|
+
}
|
|
351
|
+
get features() {
|
|
352
|
+
var _a;
|
|
353
|
+
return ((_a = this._features) !== null && _a !== void 0 ? _a : (this._features = featureSet(this._featureMask)));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
class CompiledStatic extends Compiled {
|
|
357
|
+
constructor(regex, featureMask) {
|
|
358
|
+
super(featureMask);
|
|
359
|
+
this.regex = regex;
|
|
360
|
+
this.kind = "static";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
class CompiledDynamic extends Compiled {
|
|
364
|
+
constructor(dynamic, featureMask) {
|
|
365
|
+
super(featureMask);
|
|
366
|
+
this.dynamic = dynamic;
|
|
367
|
+
this.kind = "dynamic";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
296
370
|
export const compilePartial = (regex) => {
|
|
297
|
-
const { parts, groupCount,
|
|
371
|
+
const { parts, groupCount, featureMask } = walk(regex);
|
|
372
|
+
const flags = regex.flags;
|
|
298
373
|
if (!MAYBE_HAS_BACKREFERENCE_REGEX.test(regex.source)) {
|
|
299
|
-
return
|
|
300
|
-
kind: "static",
|
|
301
|
-
regex: new RegExp(render(parts, backrefToken), regex.flags),
|
|
302
|
-
features
|
|
303
|
-
};
|
|
374
|
+
return new CompiledStatic(new RegExp(render(parts, backrefToken), flags), featureMask);
|
|
304
375
|
}
|
|
305
376
|
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
306
377
|
const sanitisedParts = isUnicode
|
|
@@ -308,27 +379,19 @@ export const compilePartial = (regex) => {
|
|
|
308
379
|
: reclassifyOctalEscapes(parts, regex.source, groupCount);
|
|
309
380
|
const backreferences = sanitisedParts.filter(isBackreference);
|
|
310
381
|
if (backreferences.length === 0) {
|
|
311
|
-
return
|
|
312
|
-
kind: "static",
|
|
313
|
-
regex: new RegExp(render(sanitisedParts, backrefToken), regex.flags),
|
|
314
|
-
features
|
|
315
|
-
};
|
|
382
|
+
return new CompiledStatic(new RegExp(render(sanitisedParts, backrefToken), flags), featureMask);
|
|
316
383
|
}
|
|
317
|
-
return {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
: expandCaptured(captured, isUnicode);
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
};
|
|
384
|
+
return new CompiledDynamic({
|
|
385
|
+
originalCaptureScan: new RegExp(spliceOriginalSource(regex.source, backreferences), flags),
|
|
386
|
+
preScan: new RegExp(render(parts, () => ANY_CAPTURED_TEXT), flags),
|
|
387
|
+
expand: (capture) => render(parts, (backref) => {
|
|
388
|
+
var _a;
|
|
389
|
+
const captured = isNumericBackreference(backref)
|
|
390
|
+
? capture[backref.ref]
|
|
391
|
+
: (_a = capture.groups) === null || _a === void 0 ? void 0 : _a[backref.ref];
|
|
392
|
+
return captured === undefined
|
|
393
|
+
? "(?:" + backrefToken(backref) + DISJUNCTION_TO_END_OF_INPUT
|
|
394
|
+
: expandCaptured(captured, isUnicode);
|
|
395
|
+
})
|
|
396
|
+
}, featureMask);
|
|
334
397
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RegexFeature } from "./compilePartial.ts";
|
|
2
2
|
export type { RegexFeature };
|
|
3
|
+
declare const compiledPartial: unique symbol;
|
|
3
4
|
/**
|
|
4
5
|
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
5
6
|
*
|
|
@@ -27,9 +28,25 @@ export type { RegexFeature };
|
|
|
27
28
|
* @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
|
|
28
29
|
*/
|
|
29
30
|
declare class PartialMatchRegExp extends RegExp {
|
|
30
|
-
|
|
31
|
-
readonly features: ReadonlySet<RegexFeature>;
|
|
31
|
+
private [compiledPartial];
|
|
32
32
|
constructor(pattern: RegExp | string, flags?: string);
|
|
33
|
+
/**
|
|
34
|
+
* The syntactic constructs the original pattern uses, recorded as a side
|
|
35
|
+
* effect of the single walk that builds the partial-match regex.
|
|
36
|
+
*
|
|
37
|
+
* The set is built on first read and cached, so patterns that are only ever
|
|
38
|
+
* matched against never pay for it. It iterates in `RegexFeature` declaration
|
|
39
|
+
* order, not the order the constructs appear in the pattern.
|
|
40
|
+
*
|
|
41
|
+
* @returns The features the original pattern contains
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* new PartialMatchRegExp(/^[a-z]+/).features.has("startAnchor"); // true
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
get features(): ReadonlySet<RegexFeature>;
|
|
33
49
|
exec(input: string): RegExpExecArray | null;
|
|
50
|
+
private _execDynamic;
|
|
34
51
|
}
|
|
35
52
|
export default PartialMatchRegExp;
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
-
};
|
|
7
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
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
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
-
};
|
|
12
|
-
var _PartialMatchRegExp_instances, _PartialMatchRegExp_compiledPartial, _PartialMatchRegExp_execDynamic;
|
|
13
1
|
import { compilePartial } from "./compilePartial.js";
|
|
2
|
+
const compiledPartial = Symbol("compiledPartial");
|
|
14
3
|
/**
|
|
15
4
|
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
16
5
|
*
|
|
@@ -40,48 +29,63 @@ import { compilePartial } from "./compilePartial.js";
|
|
|
40
29
|
class PartialMatchRegExp extends RegExp {
|
|
41
30
|
constructor(pattern, flags) {
|
|
42
31
|
super(pattern, flags);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
this[compiledPartial] = compilePartial(this);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The syntactic constructs the original pattern uses, recorded as a side
|
|
36
|
+
* effect of the single walk that builds the partial-match regex.
|
|
37
|
+
*
|
|
38
|
+
* The set is built on first read and cached, so patterns that are only ever
|
|
39
|
+
* matched against never pay for it. It iterates in `RegexFeature` declaration
|
|
40
|
+
* order, not the order the constructs appear in the pattern.
|
|
41
|
+
*
|
|
42
|
+
* @returns The features the original pattern contains
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* new PartialMatchRegExp(/^[a-z]+/).features.has("startAnchor"); // true
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
get features() {
|
|
50
|
+
return this[compiledPartial].features;
|
|
47
51
|
}
|
|
48
52
|
exec(input) {
|
|
49
|
-
const compiled =
|
|
53
|
+
const compiled = this[compiledPartial];
|
|
50
54
|
if (compiled.kind === "dynamic")
|
|
51
|
-
return
|
|
55
|
+
return this._execDynamic(compiled.dynamic, input);
|
|
52
56
|
const { regex } = compiled;
|
|
53
57
|
const match = execFrom(regex, input, this.lastIndex);
|
|
54
58
|
this.lastIndex = regex.lastIndex;
|
|
55
59
|
return match;
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
_execDynamic(dynamic, input) {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
const { originalCaptureScan, preScan, expand } = dynamic;
|
|
64
|
+
const honoursLastIndex = this.global || this.sticky;
|
|
65
|
+
const start = honoursLastIndex ? this.lastIndex : 0;
|
|
66
|
+
const originalMatch = super.exec(input);
|
|
67
|
+
if (isAtOrBefore(originalMatch, start))
|
|
68
|
+
return originalMatch;
|
|
69
|
+
let preScanMatch = null;
|
|
70
|
+
if (originalMatch) {
|
|
71
|
+
preScanMatch = execFrom(preScan, input, start);
|
|
72
|
+
const noEarlierPartialPossible = preScanMatch === null || preScanMatch.index >= originalMatch.index;
|
|
73
|
+
if (noEarlierPartialPossible)
|
|
74
|
+
return originalMatch;
|
|
75
|
+
}
|
|
76
|
+
const capture = (_b = (_a = execFrom(originalCaptureScan, input, start)) !== null && _a !== void 0 ? _a : preScanMatch) !== null && _b !== void 0 ? _b : execFrom(preScan, input, start);
|
|
77
|
+
if (capture === null)
|
|
71
78
|
return originalMatch;
|
|
79
|
+
const expanded = new RegExp(expand(capture), this.flags);
|
|
80
|
+
const match = execFrom(expanded, input, start);
|
|
81
|
+
if (match === null || isAtOrBefore(originalMatch, match.index))
|
|
82
|
+
return originalMatch;
|
|
83
|
+
preferLongerCaptures(match, capture);
|
|
84
|
+
if (honoursLastIndex)
|
|
85
|
+
this.lastIndex = expanded.lastIndex;
|
|
86
|
+
return match;
|
|
72
87
|
}
|
|
73
|
-
|
|
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
|
-
};
|
|
88
|
+
}
|
|
85
89
|
function execFrom(regex, input, start) {
|
|
86
90
|
regex.lastIndex = start;
|
|
87
91
|
return regex.exec(input);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "regex-partial-match",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A regular expression transform for partial matching",
|
|
5
5
|
"devEngines": {
|
|
6
6
|
"runtime": {
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"lint:fix": "eslint . --fix",
|
|
63
63
|
"prepare": "simple-git-hooks",
|
|
64
64
|
"prepublishOnly": "tsc -p tsconfig.build.json",
|
|
65
|
+
"ci:smoke-test-built-package": "node .github/scripts/smoke-test-built-package.ts",
|
|
65
66
|
"test": "vitest run --config ./test/vitest.config.ts",
|
|
66
67
|
"test:watch": "vitest --config ./test/vitest.config.ts"
|
|
67
68
|
},
|
|
@@ -77,11 +78,11 @@
|
|
|
77
78
|
"registry": "https://registry.npmjs.org"
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
|
-
"@typescript/lib-es2018": "file:types/lib-es2018",
|
|
81
|
-
"@typescript/lib-es2022": "file:types/lib-es2022",
|
|
82
81
|
"@eslint/js": "^9.17.0",
|
|
83
82
|
"@eslint/markdown": "^7.5.1",
|
|
84
83
|
"@types/node": "^25.9.3",
|
|
84
|
+
"@typescript/lib-es2018": "file:types/lib-es2018",
|
|
85
|
+
"@typescript/lib-es2022": "file:types/lib-es2022",
|
|
85
86
|
"eslint": "^9.17.0",
|
|
86
87
|
"jiti": "^2.6.1",
|
|
87
88
|
"lint-staged": "^15.2.11",
|