regex-partial-match 0.4.0 → 1.1.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 +207 -70
- package/lib/compilePartial.d.ts +16 -0
- package/lib/compilePartial.js +334 -0
- package/lib/escapeAtom.d.ts +1 -0
- package/lib/escapeAtom.js +5 -0
- package/lib/extend.d.ts +3 -4
- package/lib/extend.js +2 -2
- package/lib/partialMatchRegExp.d.ts +35 -0
- package/lib/partialMatchRegExp.js +114 -0
- package/package.json +13 -7
- package/lib/index.d.ts +0 -31
- package/lib/index.js +0 -199
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import escapeAtom from "./escapeAtom.js";
|
|
2
|
+
const OCCURRENCES_REGEX = /\{\d+,?\d*\}/y;
|
|
3
|
+
const NOT_NUMBERS_REGEX = /\D/g;
|
|
4
|
+
const DISJUNCTION_TO_END_OF_INPUT = "|$(?![\\s\\S]))";
|
|
5
|
+
const MAYBE_HAS_BACKREFERENCE = /\\[1-9]|\\k</;
|
|
6
|
+
const isBackreference = (part) => typeof part !== "string";
|
|
7
|
+
const isNumericBackreference = (part) => isBackreference(part) && typeof part.ref === "number";
|
|
8
|
+
function walk(regex) {
|
|
9
|
+
const source = regex.source;
|
|
10
|
+
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
11
|
+
let i = 0;
|
|
12
|
+
let groupCount = 0;
|
|
13
|
+
const features = new Set();
|
|
14
|
+
function extractSlice(length) {
|
|
15
|
+
return source.slice(i, (i += length));
|
|
16
|
+
}
|
|
17
|
+
function process() {
|
|
18
|
+
var _a;
|
|
19
|
+
const result = [];
|
|
20
|
+
function appendOptional(length) {
|
|
21
|
+
result.push("(?:" + extractSlice(length) + DISJUNCTION_TO_END_OF_INPUT);
|
|
22
|
+
}
|
|
23
|
+
function appendRaw(length) {
|
|
24
|
+
result.push(extractSlice(length));
|
|
25
|
+
}
|
|
26
|
+
function appendRawGroup(prefixLength) {
|
|
27
|
+
const start = i;
|
|
28
|
+
i += prefixLength;
|
|
29
|
+
process();
|
|
30
|
+
result.push(source.slice(start, i));
|
|
31
|
+
}
|
|
32
|
+
while (i < source.length) {
|
|
33
|
+
switch (source[i]) {
|
|
34
|
+
case "\\":
|
|
35
|
+
switch (source[i + 1]) {
|
|
36
|
+
case "c":
|
|
37
|
+
features.add("controlLetterEscape");
|
|
38
|
+
appendOptional(3);
|
|
39
|
+
break;
|
|
40
|
+
case "k": {
|
|
41
|
+
const referenceEnd = source[i + 2] === "<" ? source.indexOf(">", i) : -1;
|
|
42
|
+
if (referenceEnd === -1) {
|
|
43
|
+
appendOptional(2);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
features.add("namedBackreference");
|
|
47
|
+
const start = i;
|
|
48
|
+
const ref = source.slice(i + 3, referenceEnd);
|
|
49
|
+
i = referenceEnd + 1;
|
|
50
|
+
result.push({ ref, start, end: i });
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "u":
|
|
55
|
+
features.add("unicodeEscapeSequence");
|
|
56
|
+
if (isUnicode && source[i + 2] === "{") {
|
|
57
|
+
appendOptional(source.indexOf("}", i) - i + 1);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
appendOptional(6);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "p":
|
|
64
|
+
case "P":
|
|
65
|
+
if (isUnicode) {
|
|
66
|
+
features.add("unicodePropertyEscape");
|
|
67
|
+
appendOptional(source.indexOf("}", i) - i + 1);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
appendOptional(2);
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "x":
|
|
74
|
+
features.add("hexEscapeSequence");
|
|
75
|
+
appendOptional(4);
|
|
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;
|
|
93
|
+
case "1":
|
|
94
|
+
case "2":
|
|
95
|
+
case "3":
|
|
96
|
+
case "4":
|
|
97
|
+
case "5":
|
|
98
|
+
case "6":
|
|
99
|
+
case "7":
|
|
100
|
+
case "8":
|
|
101
|
+
case "9": {
|
|
102
|
+
features.add("backreference");
|
|
103
|
+
NOT_NUMBERS_REGEX.lastIndex = i + 1;
|
|
104
|
+
const nextNonDigit = NOT_NUMBERS_REGEX.exec(source);
|
|
105
|
+
const start = i;
|
|
106
|
+
const end = nextNonDigit ? nextNonDigit.index : source.length;
|
|
107
|
+
const ref = Number(source.slice(start + 1, end));
|
|
108
|
+
i = end;
|
|
109
|
+
result.push({ ref, start, end });
|
|
110
|
+
break;
|
|
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;
|
|
121
|
+
default:
|
|
122
|
+
features.add("otherEscape");
|
|
123
|
+
appendOptional(2);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case "[": {
|
|
128
|
+
features.add("characterClass");
|
|
129
|
+
let depth = 1, j = i + 1, previousSetOperatorCharacter;
|
|
130
|
+
while (depth) {
|
|
131
|
+
const character = source[j];
|
|
132
|
+
switch (character) {
|
|
133
|
+
case "\\":
|
|
134
|
+
j += 2;
|
|
135
|
+
previousSetOperatorCharacter = undefined;
|
|
136
|
+
continue;
|
|
137
|
+
case "[":
|
|
138
|
+
if (regex.unicodeSets) {
|
|
139
|
+
features.add("nestedCharacterClass");
|
|
140
|
+
depth++;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case "]":
|
|
144
|
+
depth--;
|
|
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;
|
|
156
|
+
}
|
|
157
|
+
previousSetOperatorCharacter = character;
|
|
158
|
+
j++;
|
|
159
|
+
}
|
|
160
|
+
appendOptional(j - i);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
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;
|
|
175
|
+
case "*":
|
|
176
|
+
case "+":
|
|
177
|
+
case "?":
|
|
178
|
+
features.add("quantifier");
|
|
179
|
+
appendRaw(1);
|
|
180
|
+
break;
|
|
181
|
+
case "{": {
|
|
182
|
+
OCCURRENCES_REGEX.lastIndex = i;
|
|
183
|
+
const regExpExecArray = OCCURRENCES_REGEX.exec(source);
|
|
184
|
+
if (regExpExecArray) {
|
|
185
|
+
features.add("quantifier");
|
|
186
|
+
appendRaw(regExpExecArray[0].length);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
appendOptional(1);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "(":
|
|
194
|
+
if (source[i + 1] == "?") {
|
|
195
|
+
switch (source[i + 2]) {
|
|
196
|
+
case ":":
|
|
197
|
+
features.add("nonCapturingGroup");
|
|
198
|
+
result.push("(?:");
|
|
199
|
+
i += 3;
|
|
200
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
201
|
+
break;
|
|
202
|
+
case "=":
|
|
203
|
+
features.add("lookahead");
|
|
204
|
+
result.push("(?=");
|
|
205
|
+
i += 3;
|
|
206
|
+
result.push(...process(), ")");
|
|
207
|
+
break;
|
|
208
|
+
case "-":
|
|
209
|
+
case "i":
|
|
210
|
+
case "s":
|
|
211
|
+
case "m": {
|
|
212
|
+
const flagsStart = i + 2, colonIndex = source.indexOf(":", flagsStart);
|
|
213
|
+
const modifiers = source.slice(flagsStart, colonIndex);
|
|
214
|
+
features.add(modifiers.includes("-")
|
|
215
|
+
? "modifierGroupWithRemoval"
|
|
216
|
+
: "modifierGroup");
|
|
217
|
+
result.push("(?" + modifiers + ":");
|
|
218
|
+
i = colonIndex + 1;
|
|
219
|
+
result.push(...process(), ")");
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "!":
|
|
223
|
+
features.add("negativeLookahead");
|
|
224
|
+
appendRawGroup(3);
|
|
225
|
+
break;
|
|
226
|
+
case "<":
|
|
227
|
+
switch (source[i + 3]) {
|
|
228
|
+
case "=":
|
|
229
|
+
features.add("lookbehind");
|
|
230
|
+
appendRawGroup(4);
|
|
231
|
+
break;
|
|
232
|
+
case "!":
|
|
233
|
+
features.add("negativeLookbehind");
|
|
234
|
+
appendRawGroup(4);
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
features.add("namedGroup");
|
|
238
|
+
features.add("capturingGroup");
|
|
239
|
+
++groupCount;
|
|
240
|
+
appendRaw(source.indexOf(">", i) - i + 1);
|
|
241
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
features.add("capturingGroup");
|
|
249
|
+
++groupCount;
|
|
250
|
+
appendRaw(1);
|
|
251
|
+
result.push(...process(), DISJUNCTION_TO_END_OF_INPUT);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case ")":
|
|
255
|
+
++i;
|
|
256
|
+
return result;
|
|
257
|
+
default:
|
|
258
|
+
features.add("patternCharacter");
|
|
259
|
+
appendOptional(isUnicode && ((_a = source.codePointAt(i)) !== null && _a !== void 0 ? _a : 0) > 0xffff ? 2 : 1);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
return { parts: process(), groupCount, features };
|
|
266
|
+
}
|
|
267
|
+
function render(parts, renderBackref) {
|
|
268
|
+
return parts
|
|
269
|
+
.map((part) => (isBackreference(part) ? renderBackref(part) : part))
|
|
270
|
+
.join("");
|
|
271
|
+
}
|
|
272
|
+
function backrefToken(backref) {
|
|
273
|
+
return isNumericBackreference(backref)
|
|
274
|
+
? "\\" + String(backref.ref)
|
|
275
|
+
: "\\k<" + backref.ref + ">";
|
|
276
|
+
}
|
|
277
|
+
function reclassifyOctalEscapes(parts, source, groupCount) {
|
|
278
|
+
return parts.map((part) => isNumericBackreference(part) && part.ref > groupCount
|
|
279
|
+
? "(?:" + source.slice(part.start, part.end) + DISJUNCTION_TO_END_OF_INPUT
|
|
280
|
+
: part);
|
|
281
|
+
}
|
|
282
|
+
function spliceOriginalSource(source, backrefs) {
|
|
283
|
+
let result = "";
|
|
284
|
+
let cursor = 0;
|
|
285
|
+
for (const { start, end } of backrefs) {
|
|
286
|
+
result += source.slice(cursor, start) + "(?:[\\s\\S]*?)";
|
|
287
|
+
cursor = end;
|
|
288
|
+
}
|
|
289
|
+
return result + source.slice(cursor);
|
|
290
|
+
}
|
|
291
|
+
function expandCaptured(value, isUnicode) {
|
|
292
|
+
return (isUnicode ? Array.from(value) : value.split(""))
|
|
293
|
+
.map((atom) => "(?:" + escapeAtom(atom) + DISJUNCTION_TO_END_OF_INPUT)
|
|
294
|
+
.join("");
|
|
295
|
+
}
|
|
296
|
+
export const compilePartial = (regex) => {
|
|
297
|
+
const { parts, groupCount, features } = walk(regex);
|
|
298
|
+
if (!MAYBE_HAS_BACKREFERENCE.test(regex.source)) {
|
|
299
|
+
return {
|
|
300
|
+
kind: "static",
|
|
301
|
+
regex: new RegExp(render(parts, backrefToken), regex.flags),
|
|
302
|
+
features
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const isUnicode = regex.unicode || regex.unicodeSets;
|
|
306
|
+
const sanitizedParts = isUnicode
|
|
307
|
+
? parts
|
|
308
|
+
: reclassifyOctalEscapes(parts, regex.source, groupCount);
|
|
309
|
+
const backreferences = sanitizedParts.filter(isBackreference);
|
|
310
|
+
if (backreferences.length === 0) {
|
|
311
|
+
return {
|
|
312
|
+
kind: "static",
|
|
313
|
+
regex: new RegExp(render(sanitizedParts, backrefToken), regex.flags),
|
|
314
|
+
features
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
kind: "dynamic",
|
|
319
|
+
features,
|
|
320
|
+
dynamic: {
|
|
321
|
+
originalCaptureScan: new RegExp(spliceOriginalSource(regex.source, backreferences), regex.flags),
|
|
322
|
+
preScan: new RegExp(render(parts, () => "(?:[\\s\\S]*?)"), regex.flags),
|
|
323
|
+
expand: (capture) => render(parts, (backref) => {
|
|
324
|
+
var _a;
|
|
325
|
+
const captured = isNumericBackreference(backref)
|
|
326
|
+
? capture[backref.ref]
|
|
327
|
+
: (_a = capture.groups) === null || _a === void 0 ? void 0 : _a[backref.ref];
|
|
328
|
+
return captured === undefined
|
|
329
|
+
? "(?:" + backrefToken(backref) + DISJUNCTION_TO_END_OF_INPUT
|
|
330
|
+
: expandCaptured(captured, isUnicode);
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function escapeAtom(atom: string): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export default function escapeAtom(atom) {
|
|
2
|
+
var _a, _b;
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- support for RegExp.escape is not yet widespread, so fallback to manual escape
|
|
4
|
+
return (_b = (_a = RegExp.escape) === null || _a === void 0 ? void 0 : _a.call(RegExp, atom)) !== null && _b !== void 0 ? _b : atom.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
package/lib/extend.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import PartialMatchRegExp from "./partialMatchRegExp.ts";
|
|
1
2
|
declare global {
|
|
2
3
|
interface RegExp {
|
|
3
4
|
/**
|
|
@@ -7,7 +8,7 @@ declare global {
|
|
|
7
8
|
* with an alternation to end-of-input (`$`), allowing the pattern to match prefixes
|
|
8
9
|
* of the original pattern. This enables validation of incomplete input strings.
|
|
9
10
|
*
|
|
10
|
-
* @returns A new
|
|
11
|
+
* @returns A new PartialMatchRegExp that matches partial strings of the original pattern
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* ```typescript
|
|
@@ -23,13 +24,11 @@ declare global {
|
|
|
23
24
|
*
|
|
24
25
|
* @remarks
|
|
25
26
|
* - The transformed regex will always match an empty string at the end of input
|
|
26
|
-
* - Backreferences cannot be partially matched as they are atomic
|
|
27
27
|
* - Use with a start anchor (`^`) to prevent false positives from empty string matches
|
|
28
28
|
* - The `y` (sticky) flag may not behave as expected in partial matching scenarios
|
|
29
29
|
*
|
|
30
30
|
* @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
|
|
31
31
|
*/
|
|
32
|
-
toPartialMatchRegex():
|
|
32
|
+
toPartialMatchRegex(): PartialMatchRegExp;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
export {};
|
package/lib/extend.js
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RegexFeature } from "./compilePartial.ts";
|
|
2
|
+
export type { RegexFeature };
|
|
3
|
+
/**
|
|
4
|
+
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
5
|
+
*
|
|
6
|
+
* An instance behaves like a normal `RegExp` but also matches any input string
|
|
7
|
+
* that is a valid prefix of the original pattern — i.e. strings that could still
|
|
8
|
+
* lead to a full match with more input. This enables validation of incomplete
|
|
9
|
+
* input strings.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const pattern = /^hello world/;
|
|
14
|
+
* const partial = new PartialMatchRegExp(pattern);
|
|
15
|
+
*
|
|
16
|
+
* partial.test('h'); // true - could match
|
|
17
|
+
* partial.test('hello'); // true - could match
|
|
18
|
+
* partial.test('hello world'); // true - full match
|
|
19
|
+
* partial.test('goodbye'); // false - cannot match
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* - The transformed pattern always matches an empty string at the end of input;
|
|
24
|
+
* use a start anchor (`^`) to prevent false positives from empty string matches
|
|
25
|
+
* - The `y` (sticky) flag may not behave as expected in partial matching scenarios
|
|
26
|
+
*
|
|
27
|
+
* @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
|
|
28
|
+
*/
|
|
29
|
+
declare class PartialMatchRegExp extends RegExp {
|
|
30
|
+
#private;
|
|
31
|
+
readonly features: ReadonlySet<RegexFeature>;
|
|
32
|
+
constructor(pattern: RegExp | string, flags?: string);
|
|
33
|
+
exec(input: string): RegExpExecArray | null;
|
|
34
|
+
}
|
|
35
|
+
export default PartialMatchRegExp;
|
|
@@ -0,0 +1,114 @@
|
|
|
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_static, _PartialMatchRegExp_dynamic;
|
|
13
|
+
import { compilePartial } from "./compilePartial.js";
|
|
14
|
+
/**
|
|
15
|
+
* A `RegExp` subclass that supports partial (prefix) matching.
|
|
16
|
+
*
|
|
17
|
+
* An instance behaves like a normal `RegExp` but also matches any input string
|
|
18
|
+
* that is a valid prefix of the original pattern — i.e. strings that could still
|
|
19
|
+
* lead to a full match with more input. This enables validation of incomplete
|
|
20
|
+
* input strings.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const pattern = /^hello world/;
|
|
25
|
+
* const partial = new PartialMatchRegExp(pattern);
|
|
26
|
+
*
|
|
27
|
+
* partial.test('h'); // true - could match
|
|
28
|
+
* partial.test('hello'); // true - could match
|
|
29
|
+
* partial.test('hello world'); // true - full match
|
|
30
|
+
* partial.test('goodbye'); // false - cannot match
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* - The transformed pattern always matches an empty string at the end of input;
|
|
35
|
+
* use a start anchor (`^`) to prevent false positives from empty string matches
|
|
36
|
+
* - The `y` (sticky) flag may not behave as expected in partial matching scenarios
|
|
37
|
+
*
|
|
38
|
+
* @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
|
|
39
|
+
*/
|
|
40
|
+
class PartialMatchRegExp extends RegExp {
|
|
41
|
+
constructor(pattern, flags) {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
super(pattern, flags);
|
|
44
|
+
_PartialMatchRegExp_static.set(this, void 0);
|
|
45
|
+
_PartialMatchRegExp_dynamic.set(this, void 0);
|
|
46
|
+
const compiled = compilePartial(this);
|
|
47
|
+
_a = this, _b = this, [({ set value(_c) { __classPrivateFieldSet(_a, _PartialMatchRegExp_dynamic, _c, "f"); } }).value, ({ set value(_c) { __classPrivateFieldSet(_b, _PartialMatchRegExp_static, _c, "f"); } }).value] = compiled.kind === "dynamic"
|
|
48
|
+
? [compiled.dynamic, null]
|
|
49
|
+
: [null, compiled.regex];
|
|
50
|
+
this.features = compiled.features;
|
|
51
|
+
}
|
|
52
|
+
exec(input) {
|
|
53
|
+
if (!__classPrivateFieldGet(this, _PartialMatchRegExp_dynamic, "f")) {
|
|
54
|
+
const partial = __classPrivateFieldGet(this, _PartialMatchRegExp_static, "f"); // eslint-disable-line @typescript-eslint/no-non-null-assertion -- #static is set whenever #dynamic is null
|
|
55
|
+
partial.lastIndex = this.lastIndex;
|
|
56
|
+
const match = partial.exec(input);
|
|
57
|
+
this.lastIndex = partial.lastIndex;
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
const start = this.lastIndex;
|
|
61
|
+
const originalMatch = super.exec(input);
|
|
62
|
+
if (originalMatch)
|
|
63
|
+
return originalMatch;
|
|
64
|
+
const { originalCaptureScan, preScan, expand } = __classPrivateFieldGet(this, _PartialMatchRegExp_dynamic, "f");
|
|
65
|
+
originalCaptureScan.lastIndex = start;
|
|
66
|
+
let capture = originalCaptureScan.exec(input);
|
|
67
|
+
if (capture === null) {
|
|
68
|
+
preScan.lastIndex = start;
|
|
69
|
+
capture = preScan.exec(input);
|
|
70
|
+
}
|
|
71
|
+
if (capture === null)
|
|
72
|
+
return null;
|
|
73
|
+
const expanded = new RegExp(expand(capture), this.flags);
|
|
74
|
+
expanded.lastIndex = start;
|
|
75
|
+
const match = expanded.exec(input);
|
|
76
|
+
if (match === null)
|
|
77
|
+
return null;
|
|
78
|
+
preferLongerCaptures(match, capture);
|
|
79
|
+
this.lastIndex = expanded.lastIndex;
|
|
80
|
+
return match;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
_PartialMatchRegExp_static = new WeakMap(), _PartialMatchRegExp_dynamic = new WeakMap();
|
|
84
|
+
function pickLonger(scanned, matched) {
|
|
85
|
+
return scanned !== undefined &&
|
|
86
|
+
matched !== undefined &&
|
|
87
|
+
scanned.length > matched.length
|
|
88
|
+
? scanned
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
function preferLongerCaptures(match, scanned) {
|
|
92
|
+
var _a, _b, _c, _d;
|
|
93
|
+
for (let index = 1; index < match.length; index++) {
|
|
94
|
+
const longer = pickLonger(scanned[index], match[index]);
|
|
95
|
+
if (longer === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
match[index] = longer;
|
|
98
|
+
if (match.indices && ((_a = scanned.indices) === null || _a === void 0 ? void 0 : _a[index])) {
|
|
99
|
+
match.indices[index] = scanned.indices[index];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (match.groups && scanned.groups) {
|
|
103
|
+
for (const name of Object.keys(match.groups)) {
|
|
104
|
+
const longer = pickLonger(scanned.groups[name], match.groups[name]);
|
|
105
|
+
if (longer === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
match.groups[name] = longer;
|
|
108
|
+
if ((_b = match.indices) === null || _b === void 0 ? void 0 : _b.groups) {
|
|
109
|
+
match.indices.groups[name] = (_d = (_c = scanned.indices) === null || _c === void 0 ? void 0 : _c.groups) === null || _d === void 0 ? void 0 : _d[name];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export default PartialMatchRegExp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "regex-partial-match",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A regular expression transform for partial matching",
|
|
5
5
|
"devEngines": {
|
|
6
6
|
"runtime": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"packageManager": {
|
|
11
11
|
"name": "npm",
|
|
12
|
-
"version": "
|
|
12
|
+
"version": "11.17.0+sha512.3eeaf18997b11070d313849268b23766b9db0068997dec9471073170fe43fa17f2b4d0337bf0f52330ee2274e7f5754b21b01052742e48f5c9c74d8b1e32ef43"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
}
|
|
34
34
|
],
|
|
35
35
|
"type": "module",
|
|
36
|
-
"main": "lib/
|
|
36
|
+
"main": "lib/partialMatchRegExp.js",
|
|
37
37
|
"exports": {
|
|
38
38
|
".": {
|
|
39
|
-
"types": "./lib/
|
|
40
|
-
"import": "./lib/
|
|
41
|
-
"default": "./lib/
|
|
39
|
+
"types": "./lib/partialMatchRegExp.d.ts",
|
|
40
|
+
"import": "./lib/partialMatchRegExp.js",
|
|
41
|
+
"default": "./lib/partialMatchRegExp.js"
|
|
42
42
|
},
|
|
43
43
|
"./extend": {
|
|
44
44
|
"types": "./lib/extend.d.ts",
|
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
"type": "git",
|
|
54
54
|
"url": "git+https://github.com/TomStrepsil/regex-partial-match.git"
|
|
55
55
|
},
|
|
56
|
+
"workspaces": [
|
|
57
|
+
"benchmarking"
|
|
58
|
+
],
|
|
56
59
|
"sideEffects": false,
|
|
57
60
|
"scripts": {
|
|
58
61
|
"lint": "eslint .",
|
|
@@ -74,13 +77,16 @@
|
|
|
74
77
|
"registry": "https://registry.npmjs.org"
|
|
75
78
|
},
|
|
76
79
|
"devDependencies": {
|
|
80
|
+
"@typescript/lib-es2018": "file:types/lib-es2018",
|
|
81
|
+
"@typescript/lib-es2022": "file:types/lib-es2022",
|
|
77
82
|
"@eslint/js": "^9.17.0",
|
|
78
83
|
"@eslint/markdown": "^7.5.1",
|
|
84
|
+
"@types/node": "^25.9.3",
|
|
79
85
|
"eslint": "^9.17.0",
|
|
80
86
|
"jiti": "^2.6.1",
|
|
81
87
|
"lint-staged": "^15.2.11",
|
|
82
88
|
"simple-git-hooks": "^2.12.1",
|
|
83
|
-
"typescript": "^
|
|
89
|
+
"typescript": "^6.0.3",
|
|
84
90
|
"typescript-eslint": "^8.18.1",
|
|
85
91
|
"vitest": "^4.0.15"
|
|
86
92
|
}
|
package/lib/index.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
declare const createPartialMatchRegex: (regex: RegExp) => RegExp;
|
|
2
|
-
/**
|
|
3
|
-
* Transforms a regular expression to support partial matching.
|
|
4
|
-
*
|
|
5
|
-
* This function wraps each atomic element of the regex pattern in a non-capturing group
|
|
6
|
-
* with an alternation to end-of-input (`$`), allowing the pattern to match prefixes
|
|
7
|
-
* of the original pattern. This enables validation of incomplete input strings.
|
|
8
|
-
*
|
|
9
|
-
* @param regex - The regular expression to transform for partial matching
|
|
10
|
-
* @returns A new RegExp that matches partial strings of the original pattern
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* const pattern = /hello world/;
|
|
15
|
-
* const partial = createPartialMatchRegex(pattern);
|
|
16
|
-
*
|
|
17
|
-
* partial.test('h'); // true - could match
|
|
18
|
-
* partial.test('hello'); // true - could match
|
|
19
|
-
* partial.test('hello world'); // true - full match
|
|
20
|
-
* partial.test('goodbye'); // false - cannot match
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* @remarks
|
|
24
|
-
* - The transformed regex will always match an empty string at the end of input
|
|
25
|
-
* - Backreferences cannot be partially matched as they are atomic
|
|
26
|
-
* - Use with a start anchor (`^`) to prevent false positives from empty string matches
|
|
27
|
-
* - The `y` (sticky) flag may not behave as expected in partial matching scenarios
|
|
28
|
-
*
|
|
29
|
-
* @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
|
|
30
|
-
*/
|
|
31
|
-
export default createPartialMatchRegex;
|