regex-partial-match 1.3.0 → 2.0.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 +154 -301
- package/lib/core/corePartialMatchRegExp.d.ts +24 -0
- package/lib/core/corePartialMatchRegExp.js +22 -0
- package/lib/core/index.d.ts +4 -0
- package/lib/core/index.js +2 -0
- package/lib/extend/index.d.ts +6 -7
- package/lib/extend/index.js +1 -1
- package/lib/features/index.d.ts +20 -0
- package/lib/features/index.js +23 -0
- package/lib/hitEnd/index.d.ts +45 -0
- package/lib/hitEnd/index.js +57 -0
- package/lib/hitEnd/matchHitEnd.d.ts +4 -0
- package/lib/hitEnd/matchHitEnd.js +51 -0
- package/lib/{partialMatchRegExp/isComplete → hitEnd}/partRole.d.ts +1 -1
- package/lib/hitEnd/partRole.js +17 -0
- package/lib/hitEnd/truncationProbe.d.ts +8 -0
- package/lib/hitEnd/truncationProbe.js +187 -0
- package/lib/hitEnd/truncationProbeCache.d.ts +14 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/modules/backreferences/asNativeAtom.d.ts +2 -0
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/asNativeAtom.js +2 -2
- package/lib/modules/backreferences/asPreScanPart.d.ts +2 -0
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/asPreScanPart.js +2 -2
- package/lib/modules/backreferences/backreferenceRecorder.d.ts +2 -0
- package/lib/modules/backreferences/backreferenceRecorder.js +38 -0
- package/lib/modules/backreferences/compileDynamic.d.ts +4 -0
- package/lib/modules/backreferences/compileDynamic.js +53 -0
- package/lib/modules/backreferences/compiledBackreferences.d.ts +9 -0
- package/lib/modules/backreferences/compiledBackreferences.js +9 -0
- package/lib/modules/backreferences/constants.d.ts +2 -0
- package/lib/modules/backreferences/constants.js +2 -0
- package/lib/modules/backreferences/execDynamic.d.ts +2 -0
- package/lib/modules/backreferences/execDynamic.js +43 -0
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/flagsAtBackreference.d.ts +1 -1
- package/lib/{partialMatchRegExp → modules/backreferences}/groupName.js +1 -1
- package/lib/modules/backreferences/index.d.ts +10 -0
- package/lib/modules/backreferences/index.js +20 -0
- package/lib/modules/backreferences/longestBakedPrefixEndingInput.js +46 -0
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/renderParts.d.ts +1 -1
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/renderParts.js +1 -1
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/resolvedFromScan.d.ts +1 -1
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/resolvedFromScan.js +2 -2
- package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/startsWithUnderFlags.js +1 -1
- package/lib/modules/carets/appendMultilineCaret.d.ts +7 -0
- package/lib/modules/carets/appendMultilineCaret.js +181 -0
- package/lib/modules/carets/caretRecorder.d.ts +2 -0
- package/lib/modules/carets/caretRecorder.js +164 -0
- package/lib/modules/carets/caretSyntax.d.ts +3 -0
- package/lib/modules/carets/caretSyntax.js +11 -0
- package/lib/modules/carets/index.d.ts +9 -0
- package/lib/modules/carets/index.js +18 -0
- package/lib/modules/carets/lineTerminator.d.ts +1 -0
- package/lib/modules/carets/lineTerminator.js +31 -0
- package/lib/modules/carets/needsCaretRules.d.ts +1 -0
- package/lib/modules/carets/needsCaretRules.js +15 -0
- package/lib/modules/carets/quantifierBounds.d.ts +3 -0
- package/lib/modules/carets/quantifierBounds.js +18 -0
- package/lib/modules/index.d.ts +3 -0
- package/lib/modules/index.js +2 -0
- package/lib/partialMatchRegExp/atomSyntax.d.ts +14 -1
- package/lib/partialMatchRegExp/atomSyntax.js +24 -1
- package/lib/partialMatchRegExp/backreferenceExpansion.d.ts +1 -6
- package/lib/partialMatchRegExp/backreferenceHook.d.ts +11 -0
- package/lib/partialMatchRegExp/backreferenceHook.js +1 -0
- package/lib/partialMatchRegExp/caretHook.d.ts +17 -0
- package/lib/partialMatchRegExp/caretHook.js +5 -0
- package/lib/partialMatchRegExp/compilePartial/compileWith.d.ts +3 -0
- package/lib/partialMatchRegExp/compilePartial/compileWith.js +36 -0
- package/lib/partialMatchRegExp/compilePartial/compiled.d.ts +17 -17
- package/lib/partialMatchRegExp/compilePartial/compiled.js +8 -21
- package/lib/partialMatchRegExp/compilePartial/constants.d.ts +4 -0
- package/lib/partialMatchRegExp/compilePartial/constants.js +4 -0
- package/lib/partialMatchRegExp/compilePartial/groupShape.d.ts +4 -0
- package/lib/partialMatchRegExp/compilePartial/groupShape.js +9 -0
- package/lib/partialMatchRegExp/compilePartial/toStatic.d.ts +2 -2
- package/lib/partialMatchRegExp/compilePartial/toStatic.js +2 -2
- package/lib/partialMatchRegExp/constants.d.ts +5 -0
- package/lib/partialMatchRegExp/constants.js +5 -0
- package/lib/partialMatchRegExp/createPartialMatchRegExp.d.ts +3 -0
- package/lib/partialMatchRegExp/createPartialMatchRegExp.js +21 -0
- package/lib/partialMatchRegExp/execFrom.d.ts +1 -0
- package/lib/partialMatchRegExp/execFrom.js +4 -0
- package/lib/partialMatchRegExp/index.d.ts +49 -3
- package/lib/partialMatchRegExp/index.js +31 -3
- package/lib/partialMatchRegExp/moduleHooks.d.ts +14 -0
- package/lib/partialMatchRegExp/moduleHooks.js +15 -0
- package/lib/partialMatchRegExp/opaqueModule.d.ts +9 -0
- package/lib/partialMatchRegExp/opaqueModule.js +1 -0
- package/lib/partialMatchRegExp/part.d.ts +2 -11
- package/lib/partialMatchRegExp/partialMatchInternals.d.ts +2 -1
- package/lib/partialMatchRegExp/partialMatchInternals.js +3 -1
- package/lib/partialMatchRegExp/quantifier.d.ts +6 -0
- package/lib/partialMatchRegExp/quantifier.js +9 -0
- package/lib/partialMatchRegExp/rawLookaroundInfo.d.ts +11 -0
- package/lib/partialMatchRegExp/rawLookaroundInfo.js +1 -0
- package/lib/partialMatchRegExp/regexFeatures.d.ts +1 -5
- package/lib/partialMatchRegExp/regexFeatures.js +2 -8
- package/lib/partialMatchRegExp/scope.d.ts +12 -0
- package/lib/partialMatchRegExp/scope.js +45 -0
- package/lib/partialMatchRegExp/walk.d.ts +13 -5
- package/lib/partialMatchRegExp/walk.js +211 -165
- package/lib/partialMatchRegExp/withModules.d.ts +13 -0
- package/lib/partialMatchRegExp/withModules.js +27 -0
- package/package.json +39 -56
- package/lib/partialMatchRegExp/compilePartial/asNativeAtom.d.ts +0 -2
- package/lib/partialMatchRegExp/compilePartial/asOptionalAtom.d.ts +0 -1
- package/lib/partialMatchRegExp/compilePartial/asOptionalAtom.js +0 -4
- package/lib/partialMatchRegExp/compilePartial/asPreScanPart.d.ts +0 -2
- package/lib/partialMatchRegExp/compilePartial/index.d.ts +0 -2
- package/lib/partialMatchRegExp/compilePartial/index.js +0 -79
- package/lib/partialMatchRegExp/compilePartial/longestBakedPrefixEndingInput.js +0 -50
- package/lib/partialMatchRegExp/compilePartial/reclassifyLegacyEscapes.d.ts +0 -2
- package/lib/partialMatchRegExp/compilePartial/reclassifyLegacyEscapes.js +0 -17
- package/lib/partialMatchRegExp/compilePartial/spliceOriginalSource.d.ts +0 -2
- package/lib/partialMatchRegExp/compilePartial/spliceOriginalSource.js +0 -12
- package/lib/partialMatchRegExp/isComplete/index.d.ts +0 -38
- package/lib/partialMatchRegExp/isComplete/index.js +0 -41
- package/lib/partialMatchRegExp/isComplete/isMatchComplete.d.ts +0 -3
- package/lib/partialMatchRegExp/isComplete/isMatchComplete.js +0 -14
- package/lib/partialMatchRegExp/isComplete/partRole.js +0 -28
- package/lib/partialMatchRegExp/isComplete/truncationProbe.d.ts +0 -8
- package/lib/partialMatchRegExp/isComplete/truncationProbe.js +0 -105
- package/lib/partialMatchRegExp/isComplete/truncationProbeCache.d.ts +0 -4
- package/lib/partialMatchRegExp/partialMatchRegExp.d.ts +0 -55
- package/lib/partialMatchRegExp/partialMatchRegExp.js +0 -114
- package/lib/partialMatchRegExp/preferLongerCaptures.d.ts +0 -1
- package/lib/partialMatchRegExp/preferLongerCaptures.js +0 -30
- /package/lib/{partialMatchRegExp/isComplete → hitEnd}/truncationProbeCache.js +0 -0
- /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/caseFoldFlags.d.ts +0 -0
- /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/caseFoldFlags.js +0 -0
- /package/lib/{partialMatchRegExp → modules/backreferences}/escapeAtom.d.ts +0 -0
- /package/lib/{partialMatchRegExp → modules/backreferences}/escapeAtom.js +0 -0
- /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/flagsAtBackreference.js +0 -0
- /package/lib/{partialMatchRegExp → modules/backreferences}/groupName.d.ts +0 -0
- /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/longestBakedPrefixEndingInput.d.ts +0 -0
- /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/startsWithUnderFlags.d.ts +0 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ 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,
|
|
13
|
+
As a side effect of the parse this requires, [`features()`](#features) names the syntactic constructs a `PartialMatchRegExp`'s 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
14
|
|
|
15
15
|
**Based on an algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski)**, re-created for NPM via ISC license, with permission.
|
|
16
16
|
|
|
@@ -36,12 +36,22 @@ partial.test("hello world"); // true - full match
|
|
|
36
36
|
partial.test("goodbye"); // false - cannot match
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### Extending RegExp.prototype
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import "regex-partial-match/extend";
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
const partial = /^hello world/.toPartialMatchRegex();
|
|
45
|
+
|
|
46
|
+
partial.test("hel"); // true
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Telling a settled match from one still reading
|
|
50
|
+
|
|
51
|
+
`test()` and `exec()` answer "could this match?", which is `true` for a prefix and for a complete match alike. [`hitEnd()`](#hitend) reports whether the engine reached the end of the input while producing the match, so more input could still change it [^1]. Testing the original pattern instead doesn't answer that [^2]. Together they give progressive validation its three states:
|
|
42
52
|
|
|
43
53
|
```javascript
|
|
44
|
-
import PartialMatchRegExp, {
|
|
54
|
+
import PartialMatchRegExp, { hitEnd } from "regex-partial-match";
|
|
45
55
|
|
|
46
56
|
const partial = new PartialMatchRegExp(/^\d{4}-\d{2}-\d{2}/);
|
|
47
57
|
|
|
@@ -49,7 +59,7 @@ function state(input) {
|
|
|
49
59
|
const match = partial.exec(input);
|
|
50
60
|
|
|
51
61
|
if (match === null) return "invalid";
|
|
52
|
-
return
|
|
62
|
+
return hitEnd(partial, match) ? "incomplete" : "complete";
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
state("20xx"); // 'invalid' - reject
|
|
@@ -58,58 +68,78 @@ state("2024-06"); // 'incomplete' - no error, keep typing
|
|
|
58
68
|
state("2024-06-15"); // 'complete' - accept, enable submit
|
|
59
69
|
```
|
|
60
70
|
|
|
61
|
-
[
|
|
62
|
-
|
|
71
|
+
> [!NOTE]
|
|
72
|
+
> [`hitEnd()`](#hitend) answers "could more input change this match?", not "is this text a match?". `/hello \w+/` matches `"hello world"` in full, and `hitEnd()` is still `true`: `\w+` read the end of the input looking for more. For an exact-length pattern like the date above the two questions coincide, which is what makes the three states work; a validator for an open-ended pattern that wants "valid so far" should test the original pattern as well.
|
|
73
|
+
|
|
74
|
+
Upgrading from 1.x, where this was `isComplete()`? See the [migration guide](./codemods/transforms/v1-v2/README.md).
|
|
75
|
+
|
|
76
|
+
[^1]: Mimicking JDK's [`Matcher.hitEnd()`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--)
|
|
77
|
+
|
|
78
|
+
[^2]: Testing the original, untransformed pattern looks like it should answer this — "did the input fully satisfy the original pattern?" — but it asks a different question: whether the original matches _at all_ here, not whether _this_ match reached the end of the input on its way. The two agree almost always, but a read of the end inside a zero-width assertion can make both return an identical result by different paths. See [Why the question can't be answered from the outside](https://github.com/TomStrepsil/regex-partial-match/blob/main/docs/how-it-works.md#why-the-question-cant-be-answered-from-the-outside) for the case where they diverge.
|
|
63
79
|
|
|
64
80
|
### A note on Tree-Shaking
|
|
65
81
|
|
|
66
|
-
If your environment doesn't tree-shake (e.g. Deno, or unbundled Node)
|
|
82
|
+
If your environment doesn't tree-shake (e.g. Deno, or unbundled Node), the default entry loads [`hitEnd`](#hitend) and [`features`](#features) with the class. Import each from its own subpath instead, and only the ones you use:
|
|
67
83
|
|
|
68
84
|
```javascript
|
|
69
85
|
import PartialMatchRegExp from "regex-partial-match/partialMatchRegExp";
|
|
86
|
+
import hitEnd from "regex-partial-match/hitEnd";
|
|
87
|
+
import features from "regex-partial-match/features";
|
|
70
88
|
```
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```javascript
|
|
75
|
-
import "regex-partial-match/extend";
|
|
90
|
+
[`hitEnd`](#hitend) and [`features`](#features) accept an instance of any `PartialMatchRegExp` class, including those from [`regex-partial-match/core`](#the-lean-entry-regex-partial-matchcore).
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
### The lean entry: `regex-partial-match/core`
|
|
78
93
|
|
|
79
|
-
partial.
|
|
80
|
-
```
|
|
94
|
+
`regex-partial-match/core` exports a `PartialMatchRegExp` without two sets of rules, so a bundle that doesn't need them can leave them out. Each set is a module you bind back with `withModules`:
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
| Module | Import | Supplies |
|
|
97
|
+
| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `carets` | `regex-partial-match/modules/carets` | the rules for a `^` under the `m` flag that has something before it, and for a `^` in a group or positive lookahead |
|
|
99
|
+
| `backreferences` | `regex-partial-match/modules/backreferences` | matching a backreference against the text its group captured |
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
Both are also named exports of `regex-partial-match/modules`.
|
|
85
102
|
|
|
86
103
|
```javascript
|
|
87
|
-
|
|
104
|
+
import PartialMatchRegExp, { withModules } from "regex-partial-match/core";
|
|
105
|
+
import carets from "regex-partial-match/modules/carets";
|
|
106
|
+
import backreferences from "regex-partial-match/modules/backreferences";
|
|
107
|
+
|
|
108
|
+
new PartialMatchRegExp(/^\d{4}-\d{2}/).test("20"); // true
|
|
109
|
+
|
|
110
|
+
const WithBoth = withModules(carets, backreferences);
|
|
111
|
+
new WithBoth(/(a|b)\1/).test("a"); // true
|
|
88
112
|
```
|
|
89
113
|
|
|
90
|
-
|
|
114
|
+
`withModules` returns a `PartialMatchRegExp` class extending `RegExp`, and the same set of modules in any order returns the same class: `withModules()` is the `core` class, and `withModules(carets, backreferences)` is the default `PartialMatchRegExp`, so `instanceof` holds between them. [`hitEnd`](#hitend) and [`features`](#features) accept instances of any of these classes.
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
A module's type is `Module`, exported from both `regex-partial-match/core` and `regex-partial-match/modules`, for annotating a list of modules: `const modules: Module[] = [carets, backreferences]`. It is opaque, so what a module holds is not part of the API, and `withModules` throws a `TypeError` for anything that isn't a module.
|
|
93
117
|
|
|
94
|
-
|
|
118
|
+
#### Choosing an entry point
|
|
95
119
|
|
|
96
|
-
|
|
97
|
-
- `/$^/` - end cannot come before start
|
|
98
|
-
- `x{2}?` - lazy quantifiers are mutually exclusive to fixed-length assertions
|
|
99
|
-
- `/[]/` - an empty character class matches nothing
|
|
100
|
-
- `/(?=b)(?=c)/` - the next character cannot be two different characters
|
|
120
|
+
If your patterns aren't known when you build, because they come from users or configuration, use `regex-partial-match`. If they are fixed, use `core`: construct each pattern once, and bind the modules construction asks for.
|
|
101
121
|
|
|
102
|
-
|
|
122
|
+
| Error when constructed | Bind |
|
|
123
|
+
| -------------------------------------------------------- | ---------------- |
|
|
124
|
+
| `TypeError: Needs the carets module` | `carets` |
|
|
125
|
+
| `TypeError: Needs the backreferences module` | `backreferences` |
|
|
126
|
+
| `TypeError: Needs the carets and backreferences modules` | both |
|
|
103
127
|
|
|
104
|
-
|
|
105
|
-
> See [Partial Match Parity](/docs/partial-match-parity.md) for full details on how the library compares to reference implementations
|
|
128
|
+
The error names every module the pattern needs, and none that is already bound.
|
|
106
129
|
|
|
107
|
-
|
|
130
|
+
**Without `carets`**, a `^` is accepted outside every group and positive lookahead and, under the `m` flag, only at the start of the pattern or of a top-level alternative, with nothing before it but `^`, `$`, `\b`, `\B`, a negative lookahead or a lookbehind: `/^a|^b/m` and `/\b^a/m` compile. A `^` inside a lookbehind or negative lookahead is always accepted, since those are kept as written: `/(?<=^)a/` compiles. Any other `^` throws `TypeError: Needs the carets module` when constructed, as `/x^a/m` and `/(?=a)^a/m` do. So does a `^` inside a group or positive lookahead, even without `m`, as in `/(^a|^b)/` and `/(?m:^a)/`, since the rule that moves such a caret in front of its group is in the carets module. The rule stays there to keep `core` small. An unquantified group whose every alternative starts with `^` matches the same with the caret written once in front of it, and `core` accepts `/^(a|b)/` where it refuses `/(^a|^b)/`. Every pattern `core` accepts is transformed exactly as by the default class.
|
|
108
131
|
|
|
109
|
-
|
|
132
|
+
**Without `backreferences`**, a pattern whose backreference must be matched at run time throws `TypeError: Needs the backreferences module` when constructed, as `/(a)\1/` does. An escape that resolves without it still compiles: in `/x\8y/` there is no group 8, so `\8` is the literal `8`.
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
## ⚙️ How It Works
|
|
112
135
|
|
|
136
|
+
The library wraps each [atomic element](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions#atoms) in a non-capturing group with a disjunction to the true end of the input, so the pattern matches any prefix of what the original would match:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
/abc/ → /(?:a|$(?![\s\S]))(?:b|$(?![\s\S]))(?:c|$(?![\s\S]))/
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [How It Works](./docs/how-it-works.md) for the full transform, where it over-accepts, how `^` is handled in groups and under the `m` flag, and how backreferences are matched.
|
|
113
143
|
|
|
114
144
|
## ✅ Supported Features
|
|
115
145
|
|
|
@@ -122,19 +152,19 @@ Whatever that scan captures (or leaves `undefined`, if the group hasn't been rea
|
|
|
122
152
|
- 🔢 [Quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Quantifier) (`*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`)
|
|
123
153
|
- 🔀 [Disjunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction) (`a|b`)
|
|
124
154
|
- 👥 [Groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Capturing_group) (capturing and non-capturing) (`(?:abc)`, `(abc)`, `(?<named>abc)`)
|
|
125
|
-
- 🔙 [Backreferences](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Backreference) (`\1`, `\k<name>`) (See [caveats](#backreferences) for known limitations)
|
|
155
|
+
- 🔙 [Backreferences](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Backreference) (`\1`, `\k<name>`) (See [caveats](./docs/caveats.md#backreferences) for known limitations)
|
|
126
156
|
- 👉 [Lookahead assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookahead_assertion) (`(?=...)`, `(?!...)`)
|
|
127
157
|
- 👈 [Lookbehind assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`)
|
|
128
158
|
- ⚓ [Input Boundaries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Input_boundary_assertion) (`^`, `$`)
|
|
129
159
|
- 🆒 [Word Boundaries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Word_boundary_assertion) (`\b`, `\B`)
|
|
130
|
-
- 🏴 [Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags): `g`, `i`, `m`, `s`, `u`, `d`, `y` (See [caveats](#sticky-flag-y) for `y`)
|
|
160
|
+
- 🏴 [Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags): `g`, `i`, `m`, `s`, `u`, `d`, `y` (See [caveats](./docs/caveats.md#sticky-flag-y) for `y`)
|
|
131
161
|
- 🎚️ [Modifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?im-s:...)`)
|
|
132
162
|
|
|
133
163
|
## 🚫 Unsupported Features
|
|
134
164
|
|
|
135
165
|
The following regex features are **not currently supported**:
|
|
136
166
|
|
|
137
|
-
- ⚠️ [Character class substrings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class#matching_strings) (`\q{abc}`) - When used independently, rather than to modify, can be included, but can't partially match. See [caveats](#
|
|
167
|
+
- ⚠️ [Character class substrings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class#matching_strings) (`\q{abc}`) - When used independently, rather than to modify, can be included, but can't partially match. See [caveats](./docs/caveats.md#string-properties).
|
|
138
168
|
|
|
139
169
|
## 🌐 Browser Compatibility
|
|
140
170
|
|
|
@@ -148,176 +178,20 @@ The library is compiled to **ES2015** (ECMAScript 6). Certain regular expression
|
|
|
148
178
|
- [**`v` (unicodeSets) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) - ES2024+
|
|
149
179
|
- [**Modifiers**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?i-ms:...)`) - ES2025+
|
|
150
180
|
|
|
151
|
-
Each of these applies only when the
|
|
181
|
+
Each of these applies only when the _original_ pattern uses the feature — everything else, including construction, `exec()`, `test()` and [`hitEnd()`](#hitend), holds to the ES2015 floor.
|
|
152
182
|
|
|
153
183
|
## ⚠️ Caveats
|
|
154
184
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
For unanchored patterns (no `^` and not using the `y` flag), the library produces an expression that always matches an empty string at the true end of the input — see [How It Works](#how-it-works). Feasibly, this is the start of a new partial match.
|
|
158
|
-
|
|
159
|
-
Hence:
|
|
160
|
-
|
|
161
|
-
```js
|
|
162
|
-
/x/.test("a") === false; /* untransformed regex */
|
|
163
|
-
/(?:x|$(?![\s\S]))/.test("a") === true; /* new PartialMatchRegExp(/x/), internally */
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
To mitigate, a start anchor (`^`) can prevent the engine from scanning forward to match the empty-string fallback at the end of the input:
|
|
167
|
-
|
|
168
|
-
```js
|
|
169
|
-
/* new PartialMatchRegExp(/^x/) matches as if it were /^(?:x|$(?![\s\S]))/ */
|
|
170
|
-
/^(?:x|$(?![\s\S]))/.test("") === true;
|
|
171
|
-
/^(?:x|$(?![\s\S]))/.test("x") === true;
|
|
172
|
-
/^(?:x|$(?![\s\S]))/.test("a") === false;
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
> [!CAUTION]
|
|
176
|
-
> In [multiline mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline), `^` still matches at the start of the string and immediately after each `\n`, so the transformed regex can attempt the empty-string fallback at the start of any line — but, since the fallback requires strict end-of-input, it only succeeds if that line start is *also* genuinely where the input ends:
|
|
177
|
-
>
|
|
178
|
-
> ```js
|
|
179
|
-
> /^(?:x|$(?![\s\S]))/m.test("x") === true;
|
|
180
|
-
> /^(?:x|$(?![\s\S]))/m.test("a\n") === true; /* '^' matches after '\n', and input truly ends there */
|
|
181
|
-
> /^(?:x|$(?![\s\S]))/m.test("a\nb") === false; /* '^' matches after '\n', but "b" remains — not genuine end-of-input */
|
|
182
|
-
> ```
|
|
183
|
-
|
|
184
|
-
The [`y` flag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) prevents matching ahead from the [`lastIndex`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex) (defaulting to `0` for a new `RegExp`):
|
|
185
|
-
|
|
186
|
-
```js
|
|
187
|
-
/(?:x|$(?![\s\S]))/y.test("x") === true;
|
|
188
|
-
/(?:x|$(?![\s\S]))/y.test("a") === false;
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
> [!CAUTION]
|
|
192
|
-
> See [caveats](#sticky-flag-y) re: resetting `lastIndex` when incrementally matching
|
|
193
|
-
|
|
194
|
-
On this basis, `.test()` should be used with caution, and a match of an empty string at the true end of the input should instead be considered "no match", if validating that which came before.
|
|
185
|
+
See [Caveats](./docs/caveats.md) for details, examples and workarounds.
|
|
195
186
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
> [`isComplete()`](#iscompletepartial-partialmatchregexp-match-regexpexecarray-boolean) answers this without a length check, and covers more than one: it reports `false` for the empty end-of-input match, since it exists only because the input ran out, and equally for a non-empty prefix like `"hello"` against `/hello world/`, which a length check would wave through.
|
|
205
|
-
>
|
|
206
|
-
> It describes a match, so ask it from `exec()` rather than `test()`:
|
|
207
|
-
>
|
|
208
|
-
> ```js
|
|
209
|
-
> import PartialMatchRegExp, { isComplete } from "regex-partial-match";
|
|
210
|
-
>
|
|
211
|
-
> const partial = new PartialMatchRegExp(/x/);
|
|
212
|
-
> const match = partial.exec("a"); // ['', index: 1, input: "a", groups: undefined]
|
|
213
|
-
>
|
|
214
|
-
> isComplete(partial, match); // false - the match depended on the input running out
|
|
215
|
-
> ```
|
|
216
|
-
>
|
|
217
|
-
> `false` is "not yet", not "never": for an unanchored `/x/`, `"a"` really is a viable prefix of `"ax"`. It is only when validating that which came before that it should be read as "no match".
|
|
218
|
-
|
|
219
|
-
> [!NOTE]
|
|
220
|
-
> A more ergonomic `test()` / `exec()` output [was explored](https://github.com/TomStrepsil/regex-partial-match/pull/51), but proved a complex problem space.
|
|
221
|
-
|
|
222
|
-
### Backreferences
|
|
223
|
-
|
|
224
|
-
`PartialMatchRegExp` supports partial matching of backreferences (`\1`, `\k<name>`) — see [Patterns with backreferences](#patterns-with-backreferences) above and [docs/backreferences.md](./docs/backreferences.md) for the algorithm. A backreference is inherently atomic — `\1` must match the complete captured text or fail — but the library resolves what each group captured from a partial input and expands the backreference into per-character partial form so matching can still proceed character-by-character in the common case.
|
|
225
|
-
|
|
226
|
-
The following cases remain atomic (full native value or exactly at true end of input, no mid-value partial matching):
|
|
227
|
-
|
|
228
|
-
- **Backreferences inside lookbehinds and negative lookarounds.** These are verbatim contexts — the value a lookbehind or negative lookahead requires must be fully present or fully absent, so there's no partial-prefix position to expand into.
|
|
229
|
-
- **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.
|
|
230
|
-
- **A forward reference, outside a lookbehind** — `\1` written before group 1 opens, or referencing it while it's still open (a self-reference inside the group's own body, e.g. `\1` in `/^(\1a)$/`). Its value can't come from the capture scan, which resolves it on a path it could never have taken, so it's left to the engine — which, per ECMAScript, always resolves it to empty there (it can't have participated yet, even on a later iteration of an enclosing quantifier). This costs nothing in practice: there's no real value being withheld. (Inside a lookbehind — already covered above — matching runs right-to-left, so a reference written first can still follow its own group's capture; that's exactly why the whole body stays atomic regardless of `forward`.)
|
|
231
|
-
- **A `\k<name>` referencing a name declared more than once**, which ECMAScript permits only across disjoint alternatives. This one is stricter than the rest — see [docs/backreferences.md](./docs/backreferences.md#duplicate-named-groups) for why, and for the workaround.
|
|
232
|
-
|
|
233
|
-
The case-folding a backreference's expansion agrees against tracks a locally-scoped `(?i:...)`/`(?-i:...)` [modifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) around that specific backreference, rather than only the pattern's own flags. This holds for a backreference the local scope makes *more* case-insensitive than the pattern is, or *less*. V8 versions before [the fix](https://issues.chromium.org/issues/447583670) released in Node.js 24.12 can still mishandle the locally-disabled case when the surrounding pattern has `i`; Chromium/Electron support depends on their bundled V8 version.
|
|
234
|
-
|
|
235
|
-
#### Prefix-ambiguous top-level alternation
|
|
236
|
-
|
|
237
|
-
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:
|
|
238
|
-
|
|
239
|
-
```javascript
|
|
240
|
-
const partial = new PartialMatchRegExp(/^(ab)\1|^(abc)\2/);
|
|
241
|
-
|
|
242
|
-
partial.test("abca"); // false — but "abca" is a valid prefix of "abcabc" via the second branch
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
> [!TIP]
|
|
246
|
-
> 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:
|
|
247
|
-
>
|
|
248
|
-
> ```javascript
|
|
249
|
-
> const partial = new PartialMatchRegExp(/^(abc)\2|^(ab)\1/);
|
|
250
|
-
>
|
|
251
|
-
> partial.test("abca"); // true
|
|
252
|
-
> ```
|
|
253
|
-
|
|
254
|
-
See [docs/backreferences.md](./docs/backreferences.md) for why this happens (the internal capture scan resolving the wrong alternative first).
|
|
255
|
-
|
|
256
|
-
[^2]:
|
|
257
|
-
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.
|
|
258
|
-
|
|
259
|
-
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.
|
|
260
|
-
|
|
261
|
-
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.
|
|
262
|
-
|
|
263
|
-
[^3]:
|
|
264
|
-
To remain lightweight, no runtime type validation is applied, so non-TypeScript consumers will be reliant on underlying errors thrown if used incorrectly.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
### Positive Lookbehinds
|
|
268
|
-
|
|
269
|
-
Whilst forming a match, a positive lookbehind must match in entirety, for the pattern to match. This is inherent in the concept of non-matching groups, since they are not match-worthy themselves, but just qualify matching atoms.
|
|
270
|
-
|
|
271
|
-
e.g.
|
|
272
|
-
|
|
273
|
-
```js
|
|
274
|
-
/(?<=foo)bar/;
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
"f" through "foo" is not a match, but "foob" is.
|
|
278
|
-
|
|
279
|
-
### Surrogate Pair Matching
|
|
280
|
-
|
|
281
|
-
In [unicode-aware mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) (`u` flag), **only whole astral characters are supported**. Partial matching of individual surrogate pairs is not supported. For example, `/😀/u` will match the complete emoji character, but not the first surrogate pair in isolation. Hence, if partially matching a byte stream, be sure to pipe via a [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) first.
|
|
282
|
-
|
|
283
|
-
### [Sticky](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) Flag (`y`)
|
|
284
|
-
|
|
285
|
-
The sticky flag is fully supported for its intended use case: **scanning within a single fixed string**. Partial matches are found only at `lastIndex`; the engine does not scan forward, and `lastIndex` advances on success or resets to `0` on failure — exactly as native sticky regexes behave.
|
|
286
|
-
|
|
287
|
-
```javascript
|
|
288
|
-
import PartialMatchRegExp from "regex-partial-match";
|
|
289
|
-
|
|
290
|
-
const partial = new PartialMatchRegExp(/hello/y);
|
|
291
|
-
|
|
292
|
-
partial.lastIndex = 2;
|
|
293
|
-
partial.test("xyhello"); // true — partial match at position 2
|
|
294
|
-
partial.test("xyworld"); // false — no match at position 2, no forward scan
|
|
295
|
-
partial.lastIndex = 2;
|
|
296
|
-
partial.test("xyhel"); // true — partial prefix "hel" at position 2
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Limitation — progressive input validation:** Because a successful match advances `lastIndex`, testing a sequence of growing strings against the same instance does not work as expected:
|
|
300
|
-
|
|
301
|
-
```javascript
|
|
302
|
-
const partial = new PartialMatchRegExp(/hello/y);
|
|
303
|
-
|
|
304
|
-
partial.test("h"); // true, lastIndex → 1
|
|
305
|
-
partial.test("he"); // false — sticky requires a match at position 1 of "he",
|
|
306
|
-
// but "e" is not a valid start of the pattern
|
|
307
|
-
partial.test("hel"); // true (lastIndex was reset to 0 by the previous failure)
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
There is no way to distinguish "scanning forward in the same string" from "testing a new, longer string", so this cannot be fixed in code. For progressive input validation, use a regex **without** the `y` flag and always test against the full input so far.
|
|
311
|
-
|
|
312
|
-
The `gy` flag combination is also fully supported: `exec()`/`test()` behave as sticky, while `match()`, `matchAll()`, `replace()`, and `replaceAll()` iterate via `exec()` as global — matching [the language specification](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky).
|
|
313
|
-
|
|
314
|
-
### "String properties"
|
|
315
|
-
|
|
316
|
-
As with surrogate pair matching, grapheme clusters / string properties can only match atomically.
|
|
317
|
-
|
|
318
|
-
Hence, `[\p{RGI_Emoji_Flag_Sequence}]` will match `🇺🇳` as a whole, but not as the individual code points of which it's comprised.
|
|
319
|
-
|
|
320
|
-
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.
|
|
187
|
+
- [**`.test()` and empty matches at the end of the input**](./docs/caveats.md#test-behaviour-and-non-matching-results-from-exec-and-match): an unanchored pattern always matches `""` at the end of the input; anchor with `^`, or ask [`hitEnd()`](#hitend)
|
|
188
|
+
- [**Backreferences**](./docs/caveats.md#backreferences): some backreferences stay atomic
|
|
189
|
+
- [Captures on a partial match](./docs/caveats.md#captures-on-a-partial-match)
|
|
190
|
+
- [Prefix-ambiguous top-level alternation](./docs/caveats.md#prefix-ambiguous-top-level-alternation)
|
|
191
|
+
- [**Positive lookbehinds**](./docs/caveats.md#positive-lookbehinds): must match in their entirety
|
|
192
|
+
- [**Surrogate pair matching**](./docs/caveats.md#surrogate-pair-matching): only whole astral characters match
|
|
193
|
+
- [**Sticky flag (`y`)**](./docs/caveats.md#sticky-flag-y): not suited to progressive input validation
|
|
194
|
+
- [**"String properties"**](./docs/caveats.md#string-properties): match atomically
|
|
321
195
|
|
|
322
196
|
## 💡 Examples
|
|
323
197
|
|
|
@@ -398,11 +272,15 @@ Useful for parsing log files, network streams, or any chunked data where records
|
|
|
398
272
|
|
|
399
273
|
## 🔌 API
|
|
400
274
|
|
|
401
|
-
### `new PartialMatchRegExp(
|
|
275
|
+
### `new PartialMatchRegExp()`
|
|
402
276
|
|
|
403
|
-
|
|
277
|
+
```typescript
|
|
278
|
+
new PartialMatchRegExp(pattern: RegExp | string, flags?: string)
|
|
279
|
+
```
|
|
404
280
|
|
|
405
|
-
|
|
281
|
+
Extends `RegExp`. An instance behaves like a normal `RegExp`; `instanceof RegExp` is `true`, and `.test()`, `.exec()`, `.match()`, `.matchAll()`, `.replace()` etc. all work as expected, but also matches any input string that is a valid prefix of the original pattern, in addition to full matches.
|
|
282
|
+
|
|
283
|
+
Available via the default entry point of the package, or via `regex-partial-match/partialMatchRegExp`; see [A note on Tree-Shaking](#a-note-on-tree-shaking).
|
|
406
284
|
|
|
407
285
|
**Parameters:**
|
|
408
286
|
|
|
@@ -413,7 +291,11 @@ Available via the default entry point of the package, or via `regex-partial-matc
|
|
|
413
291
|
|
|
414
292
|
- A `PartialMatchRegExp` instance that matches partial strings of the original pattern
|
|
415
293
|
|
|
416
|
-
### `
|
|
294
|
+
### `toPartialMatchRegex()`
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
RegExp.prototype.toPartialMatchRegex(): PartialMatchRegExp
|
|
298
|
+
```
|
|
417
299
|
|
|
418
300
|
When using `import 'regex-partial-match/extend'`, this method is added to `RegExp.prototype`.
|
|
419
301
|
|
|
@@ -421,11 +303,15 @@ When using `import 'regex-partial-match/extend'`, this method is added to `RegEx
|
|
|
421
303
|
|
|
422
304
|
- A new `PartialMatchRegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
|
|
423
305
|
|
|
424
|
-
### `
|
|
306
|
+
### `hitEnd()`
|
|
425
307
|
|
|
426
|
-
|
|
308
|
+
```typescript
|
|
309
|
+
hitEnd(partial: PartialMatchRegExp, match: RegExpExecArray): boolean
|
|
310
|
+
```
|
|
427
311
|
|
|
428
|
-
|
|
312
|
+
Reports whether the engine **reached the end of the input** while producing `match`, so that more input could change it — the contract of the JDK's [`Matcher.hitEnd()`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--): when it returns `false`, no continuation of the input changes the match's index or text. `exec()` alone cannot say: it returns the same shape of array for `"h"`, `"hello"` and `"hello world"` against `/hello world/`.
|
|
313
|
+
|
|
314
|
+
Available as a named export of the default entry point, `import { hitEnd } from 'regex-partial-match'`, or as the default export of `regex-partial-match/hitEnd`.
|
|
429
315
|
|
|
430
316
|
**Parameters:**
|
|
431
317
|
|
|
@@ -434,127 +320,92 @@ Available as a named export of the default entry point: `import { isComplete } f
|
|
|
434
320
|
|
|
435
321
|
**Returns:**
|
|
436
322
|
|
|
437
|
-
- `true` when
|
|
438
|
-
- `false` when the
|
|
323
|
+
- `true` when the match read the end of the input: an atom ran out of input and took one of the `|$(?![\s\S])` branches described in [How It Works](#how-it-works); a greedy quantifier stopped at the end because there was nothing left to read; or `$`, `\b` or `\B` held there. More input could extend the match, change which alternative wins, or invalidate it, and its captures are the closest available rather than final — see [Captures on a partial match](./docs/caveats.md#captures-on-a-partial-match).
|
|
324
|
+
- `false` when every atom matched literally and nothing read past the last character consumed. No continuation of the input changes the match's index or text, and the captures are the ones the original pattern produces — except the two cases in [What it cannot see](#what-it-cannot-see) below, where a read of the end leaves no marker and `false` is reported despite it.
|
|
439
325
|
|
|
440
326
|
```javascript
|
|
441
|
-
import PartialMatchRegExp, {
|
|
327
|
+
import PartialMatchRegExp, { hitEnd } from "regex-partial-match";
|
|
442
328
|
|
|
443
329
|
const partial = new PartialMatchRegExp(/hello world/);
|
|
444
|
-
const prefix = partial.exec("hello");
|
|
445
330
|
|
|
446
|
-
|
|
447
|
-
|
|
331
|
+
hitEnd(partial, partial.exec("hello")); // true — ran out of input inside "world"
|
|
332
|
+
hitEnd(partial, partial.exec("hello world!")); // false — settled short of the end
|
|
333
|
+
hitEnd(partial, partial.exec("hello world")); // false — consumed the last character without reading past it
|
|
448
334
|
|
|
449
|
-
const
|
|
335
|
+
const greedy = new PartialMatchRegExp(/hello \w+/);
|
|
450
336
|
|
|
451
|
-
|
|
452
|
-
isComplete(partial, complete); // true
|
|
337
|
+
hitEnd(greedy, greedy.exec("hello world")); // true — \w+ read the end looking for more
|
|
453
338
|
```
|
|
454
339
|
|
|
455
340
|
> [!NOTE]
|
|
456
|
-
>
|
|
457
|
-
|
|
458
|
-
#### Why the question can't be answered from the outside
|
|
459
|
-
|
|
460
|
-
Neither of the obvious workarounds answers it:
|
|
341
|
+
> Where the JDK is exact, [`hitEnd`](#hitend) is conservative in one place: a bounded greedy quantifier (`?`, `{n,m}`) fully taken at the end of the input reports `true`, although the engine attempted no further read there — on a _group_ (`/(ab)?/` on `"ab"` is `true` here and `false` in Java), and the same way for an unequal-bound `{n,m}` directly on a single atom once it's saturated at its maximum (`/a{1,2}/` on `"aa"` is `true`, though no continuation can add a third `a`). Outside the two limits in [What it cannot see](#what-it-cannot-see), it is never wrong in the other direction.
|
|
461
342
|
|
|
462
|
-
|
|
463
|
-
- **Re-running the original pattern.** That asks whether the original matches *at all* at that position, not whether *this result* was arrived at by truncation. Where a truncation branch fires inside a zero-width assertion the two diverge, and the original can return an identical array by a different path:
|
|
464
|
-
|
|
465
|
-
```javascript
|
|
466
|
-
const pattern = /a(?=(?:b(?:x|(c))d|b))/;
|
|
467
|
-
const partial = new PartialMatchRegExp(pattern);
|
|
468
|
-
|
|
469
|
-
partial.exec("ab"); // ['a', undefined] — truncated inside the assertion
|
|
470
|
-
pattern.exec("ab"); // ['a', undefined] — identical, and complete
|
|
471
|
-
pattern.exec("abcd"); // ['a', 'c'] — what more input actually produces
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
Over `"ab"` the transformed pattern satisfies the atoms after `b` through *their* truncation branches — zero-width, so the match's own end never moves — before ever reaching the `(c)` group. Group 1 is left `undefined` where more input would define it.
|
|
475
|
-
|
|
476
|
-
The information only exists during matching. `isComplete()` recovers it by re-running the compiled pattern, [sticky](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) at `match.index`, with an empty named group in front of each truncation branch; an empty group is zero-width and always succeeds, so the twin walks the identical path, and any marker that comes back defined is a truncation branch the match actually took.
|
|
477
|
-
|
|
478
|
-
Recognising a truncation branch, a raw lookaround, or a group open back out of the rendered pattern is one classification, shared by every pass that needs it, rather than re-derived independently wherever it's needed.
|
|
479
|
-
|
|
480
|
-
The twin is built lazily, on first use, and never escapes the library — the array, `groups`, numbering and `d`-flag indices you hold are the ones `exec()` produced. It is cached once per instance for a pattern without [backreferences](#backreferences); for one with them the pattern is re-expanded per input, so the twin belongs to the expansion behind one particular match and a later match builds its own.
|
|
481
|
-
|
|
482
|
-
**Cost:** one anchored `exec` per call, plus the twin's construction on the first call that needs it. `exec()` and `test()` are untouched: the expansion a backreference match is answered from is held on the match itself, under a private symbol, so it costs a single field write and is collected with the match. Repeatedly asking about the *same* match is cheap; asking once per match on a backreference pattern pays for a new twin each time.
|
|
483
|
-
|
|
484
|
-
> [!CAUTION]
|
|
485
|
-
> `isComplete()` itself always requires ES2018+, regardless of the pattern: its truncation probe is built from named capturing groups internally, even for a pattern as plain as `/^abc/`. See [Browser Compatibility](#browser-compatibility) — every other method holds to the ES2015 floor stated there.
|
|
343
|
+
See [How It Works](./docs/how-it-works.md#why-the-question-cant-be-answered-from-the-outside) for why this can't be worked out from the match alone, and how [`hitEnd`](#hitend) records a read of the end.
|
|
486
344
|
|
|
487
345
|
#### What it cannot see
|
|
488
346
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
```javascript
|
|
492
|
-
const partial = new PartialMatchRegExp(/a(?=(b+))/);
|
|
493
|
-
const match = partial.exec("ab");
|
|
347
|
+
- **A read of the end inside a lookahead in an earlier iteration of a quantified group.** The probe's [markers](./docs/how-it-works.md#recording-a-read-of-the-end) are capturing groups, and [`RepeatMatcher`](https://tc39.es/ecma262/#sec-runtime-semantics-repeatmatcher-abstract-operation) resets a quantified group's captures at the start of every iteration. `/(?:a(?=bcd)|b)+/` on `"abc"` reads the end inside `(?=bcd)` in its first iteration, matches `b` in its second, and reports `false` — although `"abcx"` changes the match to `"b"` at index 1. Nothing placed inside the repeated atom survives the reset, so this is a limit of the marker approach rather than an oversight, and it is pinned by a test.
|
|
348
|
+
- **A read of the end inside a raw lookaround.** Negative lookaheads and both lookbehinds are kept verbatim (see [Caveats](./docs/caveats.md)), so a read of the end inside them leaves no marker: `/^a(?!b)/` on `"a"` reports `false`, although `"ab"` invalidates the match. A scanner whose output must not depend on where its input was chunked should refuse or buffer patterns that use them.
|
|
494
349
|
|
|
495
|
-
|
|
496
|
-
isComplete(partial, match); // true
|
|
350
|
+
### `features()`
|
|
497
351
|
|
|
498
|
-
|
|
352
|
+
```typescript
|
|
353
|
+
features(partial: PartialMatchRegExp): ReadonlySet<RegexFeature>
|
|
499
354
|
```
|
|
500
355
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
A chunk-invariant scanner needs a second, independent check: whether any capture's end coincides with the end of the buffer, computable from [`d`-flag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/hasIndices) indices without this library's help. Defer a match when either check says to.
|
|
356
|
+
Building the partial-match regex requires walking the entire source pattern, usually once. [^3] As a side effect of the walk that builds the regex, each instance records which syntactic constructs its pattern actually uses, and `features()` names them as a set — no separate scan of the source is performed to produce it. The set is built on the first call for an instance and the same set is returned after that.
|
|
504
357
|
|
|
505
|
-
|
|
358
|
+
Available as a named export of the default entry point, `import { features } from 'regex-partial-match'`, or as the default export of `regex-partial-match/features`. It accepts an instance of any `PartialMatchRegExp` class, including those from [`regex-partial-match/core`](#the-lean-entry-regex-partial-matchcore).
|
|
506
359
|
|
|
507
|
-
|
|
360
|
+
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:
|
|
508
361
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
- **
|
|
512
|
-
- **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.
|
|
513
|
-
- **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)/`.
|
|
362
|
+
- **Flagging patterns likely to hit one of the [caveats](./docs/caveats.md).** For example, a pattern combining `backreference` with `lookbehind`, `negativeLookahead`, or `negativeLookbehind` is a candidate for the [atomic-backreference caveat](./docs/caveats.md#backreferences); one combining `backreference` with `disjunction` is a candidate for the [prefix-ambiguous top-level alternation caveat](./docs/caveats.md#prefix-ambiguous-top-level-alternation). A consumer accepting user-supplied patterns can surface a warning instead of letting the edge case surprise someone later.
|
|
363
|
+
- **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.
|
|
364
|
+
- **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(partial).has("lookaroundCapture")` isolates exactly those patterns, where checking for both `lookahead` and `capturingGroup` would also catch the ordinary `/(\w+)(?= END)/`.
|
|
514
365
|
|
|
515
366
|
```javascript
|
|
516
|
-
import PartialMatchRegExp from "regex-partial-match";
|
|
367
|
+
import PartialMatchRegExp, { features } from "regex-partial-match";
|
|
517
368
|
|
|
518
369
|
const partial = new PartialMatchRegExp(/^[a-z]+(?<domain>\.[a-z]+)\1/);
|
|
519
370
|
|
|
520
|
-
partial
|
|
521
|
-
partial.
|
|
371
|
+
features(partial); // Set { "startAnchor", "backreference", "namedGroup", "capturingGroup", "characterClass", "quantifier", "otherEscape" }
|
|
372
|
+
features(partial).has("backreference"); // true
|
|
522
373
|
```
|
|
523
374
|
|
|
524
|
-
`RegexFeature` is a string union, exported
|
|
525
|
-
|
|
526
|
-
| Feature | Matches
|
|
527
|
-
|
|
|
528
|
-
| `patternCharacter`
|
|
529
|
-
| `startAnchor`
|
|
530
|
-
| `endAnchor`
|
|
531
|
-
| `wordBoundary`
|
|
532
|
-
| `nonWordBoundary`
|
|
533
|
-
| `lookahead`
|
|
534
|
-
| `negativeLookahead`
|
|
535
|
-
| `lookbehind`
|
|
536
|
-
| `negativeLookbehind`
|
|
537
|
-
| `backreference`
|
|
538
|
-
| `namedBackreference`
|
|
539
|
-
| `namedGroup`
|
|
540
|
-
| `capturingGroup`
|
|
541
|
-
| `lookaroundCapture`
|
|
542
|
-
| `nonCapturingGroup`
|
|
543
|
-
| `modifierGroup`
|
|
544
|
-
| `modifierGroupWithRemoval`
|
|
545
|
-
| `characterClass`
|
|
546
|
-
| `nestedCharacterClass`
|
|
547
|
-
| `classIntersection`
|
|
548
|
-
| `classSubtraction`
|
|
549
|
-
| `disjunction`
|
|
550
|
-
| `quantifier`
|
|
551
|
-
| `unicodePropertyEscape`
|
|
552
|
-
| `characterClassEscape`
|
|
553
|
-
| `controlEscape`
|
|
554
|
-
| `controlLetterEscape`
|
|
555
|
-
| `hexEscapeSequence`
|
|
556
|
-
| `unicodeEscapeSequence`
|
|
557
|
-
| `otherEscape`
|
|
375
|
+
`RegexFeature` is a string union, exported from `regex-partial-match` and `regex-partial-match/features`:
|
|
376
|
+
|
|
377
|
+
| Feature | Matches | Notes |
|
|
378
|
+
| -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
379
|
+
| `patternCharacter` | An ordinary literal character | |
|
|
380
|
+
| `startAnchor` | Top-level `^` | |
|
|
381
|
+
| `endAnchor` | Top-level `$` | |
|
|
382
|
+
| `wordBoundary` | Top-level `\b` | |
|
|
383
|
+
| `nonWordBoundary` | Top-level `\B` | |
|
|
384
|
+
| `lookahead` | `(?=...)` | |
|
|
385
|
+
| `negativeLookahead` | `(?!...)` | |
|
|
386
|
+
| `lookbehind` | `(?<=...)` | |
|
|
387
|
+
| `negativeLookbehind` | `(?<!...)` | |
|
|
388
|
+
| `backreference` | `\1` | |
|
|
389
|
+
| `namedBackreference` | `\k<name>` | |
|
|
390
|
+
| `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
|
|
391
|
+
| `capturingGroup` | `(...)`, including named groups | |
|
|
392
|
+
| `lookaroundCapture` | A capturing group inside any lookaround | Nesting, not a construct — always accompanied by `capturingGroup` |
|
|
393
|
+
| `nonCapturingGroup` | `(?:...)` | |
|
|
394
|
+
| `modifierGroup` | `(?ims:...)` | |
|
|
395
|
+
| `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
|
|
396
|
+
| `characterClass` | `[...]` | |
|
|
397
|
+
| `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
|
|
398
|
+
| `classIntersection` | `&&` inside a character class | `v` flag only |
|
|
399
|
+
| `classSubtraction` | `--` inside a character class | `v` flag only |
|
|
400
|
+
| `disjunction` | `\|` | |
|
|
401
|
+
| `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
|
|
402
|
+
| `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only — otherwise tagged `otherEscape` |
|
|
403
|
+
| `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
|
|
404
|
+
| `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
|
|
405
|
+
| `controlLetterEscape` | `\cX` | |
|
|
406
|
+
| `hexEscapeSequence` | `\xXX` | |
|
|
407
|
+
| `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
|
|
408
|
+
| `otherEscape` | Any other `\X`, e.g. `\.` | `\0` alone is tagged `otherEscape` only under `u`/`v`; otherwise `backreference`, [like any other digit escape](docs/modules/backreferences.md) |
|
|
558
409
|
|
|
559
410
|
Three things worth knowing about how these tags line up with the grammar:
|
|
560
411
|
|
|
@@ -562,6 +413,8 @@ Three things worth knowing about how these tags line up with the grammar:
|
|
|
562
413
|
- **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.
|
|
563
414
|
- **`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.
|
|
564
415
|
|
|
416
|
+
[^3]: The [carets module](./docs/modules/carets.md) guesses from a cheap look at the source whether a pattern needs its rules, and when it guesses wrong the pattern is walked a second time with them.
|
|
417
|
+
|
|
565
418
|
## 📜 License
|
|
566
419
|
|
|
567
420
|
ISC License - see [LICENSE](./LICENSE) file for details.
|