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 CHANGED
@@ -2,31 +2,33 @@
2
2
 
3
3
  A zero-dependency regular expression transform for partial matching, enabling validation of incomplete input strings against regex patterns.
4
4
 
5
- ## Problem statement
5
+ ## 🧩 Problem statement
6
6
 
7
7
  Unlike C/C++ (via [PCRE/PCRE2](https://www.pcre.org/original/doc/html/pcrepartial.html), [RE2](https://github.com/google/re2?tab=readme-ov-file#matching-interface), [Boost.Regex](https://www.boost.org/doc/libs/1_34_1/libs/regex/doc/partial_matches.html)), Python ([via third party regex module](https://pypi.org/project/regex/#:~:text=Added%20partial%20matches)) or Java (via [`hitEnd`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--)), Javascript has no canonical / innate partial-matching for regular expressions.
8
8
 
9
- ## Overview
9
+ ## 📖 Overview
10
10
 
11
11
  This library transforms regular expressions to best-effort support **partial matching**, allowing you to test if an incomplete string could potentially match the full pattern. This is particularly useful for real-time input validation, autocomplete systems, progressive form validation, stream chunk matching, etc.
12
12
 
13
+ As a side effect of the parse this requires, each `PartialMatchRegExp` also exposes a [`features`](#partialmatchregexpprototypefeatures-readonlysetregexfeature) set naming the syntactic constructs its pattern uses — useful for consumers that need to reason about a pattern without writing their own regex parser. For many features, a simple search in the [source](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source) would be insufficient.
14
+
13
15
  **Based on an algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski)**, re-created for NPM via ISC license, with permission.
14
16
 
15
- ## Installation
17
+ ## 📦 Installation
16
18
 
17
19
  ```bash
18
20
  npm install regex-partial-match
19
21
  ```
20
22
 
21
- ## Usage
23
+ ## 🚀 Usage
22
24
 
23
25
  ### Basic Usage
24
26
 
25
27
  ```javascript
26
- import createPartialMatchRegex from "regex-partial-match";
28
+ import PartialMatchRegExp from "regex-partial-match";
27
29
 
28
- const pattern = /hello world/;
29
- const partial = createPartialMatchRegex(pattern);
30
+ const pattern = /^hello world/;
31
+ const partial = new PartialMatchRegExp(pattern);
30
32
 
31
33
  partial.test("h"); // true - could match
32
34
  partial.test("hello"); // true - could match
@@ -39,22 +41,22 @@ partial.test("goodbye"); // false - cannot match
39
41
  ```javascript
40
42
  import "regex-partial-match/extend";
41
43
 
42
- const partial = /hello world/.toPartialMatchRegex();
44
+ const partial = /^hello world/.toPartialMatchRegex();
43
45
 
44
46
  partial.test("hel"); // true
45
47
  ```
46
48
 
47
- ## How It Works
49
+ ## ⚙️ How It Works
48
50
 
49
- The library transforms a regular expression by wrapping each atomic element in a non-capturing group with an alternation to end-of-input (`$`):
51
+ The library transforms a regular expression by wrapping each [atomic element](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions#atoms) in a [non-capturing group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Non-capturing_group) with a [disjunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction) to a true-end-of-input sentinel (`$(?![\s\S])`[^1]):
50
52
 
51
53
  ```javascript
52
- /abc/ → /(?:a|$)(?:b|$)(?:c|$)/
54
+ /abc/ → /(?:a|$(?![\s\S]))(?:b|$(?![\s\S]))(?:c|$(?![\s\S]))/
53
55
  ```
54
56
 
55
57
  This allows the pattern to match prefixes of the original pattern, enabling validation of incomplete input.
56
58
 
57
- Since the library accepts only valid regular expressions [^1], this enables the algorithm to make lots of unguarded assumptions about the source of the expression.
59
+ Since the library accepts only valid regular expressions [^2], this enables the algorithm to make lots of unguarded assumptions about the source of the expression.
58
60
 
59
61
  The library has been stress-tested with various regular expression features in isolation, and some in likely combination, but obviously it's an unbounded test space, and syntactically valid regular expressions nevertheless support contradictory patterns e.g.
60
62
 
@@ -64,9 +66,17 @@ The library has been stress-tested with various regular expression features in i
64
66
 
65
67
  Such combinations have not been tested.
66
68
 
67
- [^1]: To remain lightweight, no runtime type validation is applied, so non-typescript consumers will be reliant on underlying errors thrown, if used incorrectly.
69
+ > [!NOTE]
70
+ > See [Partial Match Parity](/docs/partial-match-parity.md) for full details on how the library compares to reference implementations
71
+
72
+ ### Patterns with backreferences
73
+
74
+ Backreferences cannot be handled by the `|$(?![\s\S])` transform alone because they are atomic — `\1` must match the entire captured string or fail, and its length is only known at runtime. `PartialMatchRegExp` first tries a full match natively, as a short-circuit — if the input already satisfies the whole pattern, there's nothing further to do. Otherwise it runs a "capture scan": a variant of the pattern with each backreference swapped for a lazy `(?:[\s\S]*?)` wildcard, so the group it depends on can still capture against a partial input — matching anything, or nothing at all, without needing to already know the backreference's value.
68
75
 
69
- ## Supported Features
76
+ Whatever that scan captures (or leaves `undefined`, if the group hasn't been reached yet) is then used to build a fresh partial-matching regex for this specific input, expanding the backreference character-by-character from the captured value with the same per-atom transform as the rest of the pattern. See [docs/backreferences.md](./docs/backreferences.md) for the full algorithm, including the prefer-longer post-processing that preserves correct captures for groups inside quantifiers.
77
+
78
+
79
+ ## ✅ Supported Features
70
80
 
71
81
  - 🔤 [Literal characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Literal_character)
72
82
  - 🔣 [Character escapes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape) (`\n`, `\t`, `\x61`, `\u0061`, `\u{1F600}`)
@@ -77,6 +87,7 @@ Such combinations have not been tested.
77
87
  - 🔢 [Quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Quantifier) (`*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`)
78
88
  - 🔀 [Disjunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction) (`a|b`)
79
89
  - 👥 [Groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Capturing_group) (capturing and non-capturing) (`(?:abc)`, `(abc)`, `(?<named>abc)`)
90
+ - 🔙 [Backreferences](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Backreference) (`\1`, `\k<name>`) (See [caveats](#backreferences) for known limitations)
80
91
  - 👉 [Lookahead assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookahead_assertion) (`(?=...)`, `(?!...)`)
81
92
  - 👈 [Lookbehind assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`)
82
93
  - ⚓ [Input Boundaries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Input_boundary_assertion) (`^`, `$`)
@@ -84,18 +95,16 @@ Such combinations have not been tested.
84
95
  - 🏴 [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`)
85
96
  - 🎚️ [Modifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?im-s:...)`)
86
97
 
87
- ## Unsupported Features
98
+ ## 🚫 Unsupported Features
88
99
 
89
100
  The following regex features are **not currently supported**:
90
101
 
91
- - ⚠️ [Backreferences](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Backreference) (`\1`, `\k<name>`) - Can be included, but can't partially match. See [caveats](#caveats).
92
102
  - ⚠️ [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](#caveats).
93
103
 
94
- ## Browser Compatibility
104
+ ## 🌐 Browser Compatibility
95
105
 
96
- The library is compiled to **ES5** for broad compatibility with older browsers and JavaScript environments. However, certain regular expression features naturally require ES2015+ support:
106
+ The library is compiled to **ES2015** (ECMAScript 6). Certain regular expression features naturally require newer environments:
97
107
 
98
- - [**Unicode escapes**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) (`\u{...}`) - ES2015+
99
108
  - [**Unicode property escapes**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) (`\p{...}`, `\P{...}`) - ES2018+
100
109
  - [**Lookbehind assertions**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`) - ES2018+
101
110
  - [**Named capturing groups**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) (`(?<name>...)`) - ES2018+
@@ -104,43 +113,99 @@ The library is compiled to **ES5** for broad compatibility with older browsers a
104
113
  - [**`v` (unicodeSets) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) - ES2024+
105
114
  - [**Modifiers**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?i-ms:...)`) - ES2025+
106
115
 
107
- ## Caveats
116
+ ## ⚠️ Caveats
108
117
 
109
118
  ### [`.test()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) behaviour and non-matching results from [`.exec()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec) and [`.match()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match)
110
119
 
111
- The library produces an expression that always matches an empty string, at the end of the input. Feasibly, this is the start of a new partial match.
120
+ 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.
112
121
 
113
122
  Hence:
114
123
 
115
124
  ```js
116
125
  /x/.test("a") === false; /* untransformed regex */
117
- /(?:x|$)/.test("a") === true; /* what's produced by the library */
126
+ /(?:x|$(?![\s\S]))/.test("a") === true; /* new PartialMatchRegExp(/x/), internally */
118
127
  ```
119
128
 
120
- To mitigate, a start boundary anchor can prevent anything _but_ an empty string matching:
129
+ To mitigate, a start anchor (`^`) can prevent the engine from scanning forward to match the empty-string fallback at the end of the input:
121
130
 
122
131
  ```js
123
- /^(?:x|$)/.test("") === true;
124
- /^(?:x|$)/.test("x") === true;
125
- /^(?:x|$)/.test("a") === false;
132
+ /* new PartialMatchRegExp(/^x/) matches as if it were /^(?:x|$(?![\s\S]))/ */
133
+ /^(?:x|$(?![\s\S]))/.test("") === true;
134
+ /^(?:x|$(?![\s\S]))/.test("x") === true;
135
+ /^(?:x|$(?![\s\S]))/.test("a") === false;
126
136
  ```
127
137
 
128
- On this basis, `.test()` should be used with caution, and a match of an empty string at the end of the input should instead be considered "no match", if validating that which came before.
138
+ > [!CAUTION]
139
+ > 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:
140
+ >
141
+ > ```js
142
+ > /^(?:x|$(?![\s\S]))/m.test("x") === true;
143
+ > /^(?:x|$(?![\s\S]))/m.test("a\n") === true; /* '^' matches after '\n', and input truly ends there */
144
+ > /^(?:x|$(?![\s\S]))/m.test("a\nb") === false; /* '^' matches after '\n', but "b" remains — not genuine end-of-input */
145
+ > ```
146
+
147
+ 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`):
148
+
149
+ ```js
150
+ /(?:x|$(?![\s\S]))/y.test("x") === true;
151
+ /(?:x|$(?![\s\S]))/y.test("a") === false;
152
+ ```
153
+
154
+ > [!CAUTION]
155
+ > See [caveats](#sticky-flag-y) re: resetting `lastIndex` when incrementally matching
156
+
157
+ 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.
129
158
 
130
- i.e.
159
+ e.g.
131
160
 
132
161
  ```js
133
- /(?:x|$)/.exec("a"); // ['', index: 1, input: "a", groups: undefined];
134
- "a".match(/(?:x|$)/); // ['', index: 1, input: "a", groups: undefined];
162
+ /(?:x|$(?![\s\S]))/.exec("a"); // ['', index: 1, input: "a", groups: undefined];
163
+ "a".match(/(?:x|$(?![\s\S]))/); // ['', index: 1, input: "a", groups: undefined];
135
164
  ```
136
165
 
137
- Since the library produces a native `RegExp` object, no attempt to proxy / translate this output to `null` has been attempted, but a helper could be produced in future, for clarity. See [issue](https://github.com/TomStrepsil/regex-partial-match/issues/14).
166
+ > [!NOTE]
167
+ > A more ergonomic `test()` / `exec()` output [was explored](https://github.com/TomStrepsil/regex-partial-match/pull/51), but proved a complex problem space.
138
168
 
139
169
  ### Backreferences
140
170
 
141
- **Backreferences cannot be partially matched because they are atomic.** A backreference like `\1` must match the complete captured text or fail entirely, and cannot be split into individual characters for partial matching like regular atoms can.
171
+ `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.
172
+
173
+ The following cases remain atomic (full native value or exactly at true end of input, no mid-value partial matching):
174
+
175
+ - **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.
176
+ - **`\k<name>` with no named capturing groups in the pattern.** [Annex B](https://tc39.es/ecma262/#sec-regular-expressions-patterns) tolerates this as the literal characters `k<a>`, but it's still treated as if it were a named backreference and matched atomically — `"k"` and `"k<"` will not partially match. A `\k` not immediately followed by a well-formed `<name>` reference is treated as the literal `k` and partially matches as usual.
177
+ - **A backreference whose captured value can't be determined from a partial input.** This only affects the backreference site itself; it's strictly better than rejecting the input outright, and never accepts anything unsound.
178
+
179
+ #### Prefix-ambiguous top-level alternation
180
+
181
+ When a pattern uses top-level alternation where one branch is a strict prefix of another (e.g. `^(ab)\1|^(abc)\2`), the internal capture scan may select the shorter branch — because it uses `(?:[\s\S]*?)` which accepts zero characters — causing the final partial regex to fail for inputs that are valid prefixes of the longer branch. In such cases `exec` returns `null` even though the input is a valid partial match:
182
+
183
+ ```javascript
184
+ const partial = new PartialMatchRegExp(/^(ab)\1|^(abc)\2/);
185
+
186
+ partial.test("abca"); // false — but "abca" is a valid prefix of "abcabc" via the second branch
187
+ ```
188
+
189
+ > [!TIP]
190
+ > If alternate branches share a prefix, list the longer one first. The capture scan tries branches in order and stops at the first that accepts the partial input, so putting the longer branch first ensures it's the one selected:
191
+ >
192
+ > ```javascript
193
+ > const partial = new PartialMatchRegExp(/^(abc)\2|^(ab)\1/);
194
+ >
195
+ > partial.test("abca"); // true
196
+ > ```
197
+
198
+ See [docs/backreferences.md](./docs/backreferences.md) for why this happens (the internal capture scan resolving the wrong alternative first).
142
199
 
143
- Fixed-length patterns like `/(abc)\1/` could theoretically become `/(?:(a)|$)(?:(b)|$)(?:(c)|$)(?:\1|$)(?:\2|$)(?:\3|$)/` (accepting polluted capture indexes as a side-effect), but this doesn't work for variable-length captures.
200
+ [^1]:
201
+ A bare `$` alone isn't sufficient here: under the `m` (multiline) flag — including one turned on locally via a `(?m:...)` [modifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) — `$` also matches immediately before *any* line terminator, not just the true end of input. That would let a `"\n"` the source pattern never allowed for be silently accepted as if the input had simply run out, e.g. `new PartialMatchRegExp(/^foobar/m)` would wrongly accept `"foo\nbaz"`. Appending `(?![\s\S])` narrows the disjunction down to strict end-of-input, regardless of multiline state.
202
+
203
+ See [chromium issue 536420076](https://issues.chromium.org/u/2/issues/536420076) for the underlying V8 bug that requires `$` to precede `(?![\s\S])` rather than using the lookahead alone.
204
+
205
+ A shorter option, `(?-m:$)` — disabling multiline locally so `$` means strict end-of-input on its own — also sidesteps the bug and saves a few bytes per atom. However, modifier groups are new enough that support isn't universal, and feature-detecting them would add a fallback branch the test suite can't exercise honestly, since every engine that can realistically be tested against already supports them.
206
+
207
+ [^2]:
208
+ To remain lightweight, no runtime type validation is applied, so non-TypeScript consumers will be reliant on underlying errors thrown if used incorrectly.
144
209
 
145
210
  ### Positive Lookbehinds
146
211
 
@@ -160,27 +225,34 @@ In [unicode-aware mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/
160
225
 
161
226
  ### [Sticky](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) Flag (`y`)
162
227
 
163
- The **sticky flag may not behave as expected** in partial matching scenarios. The sticky flag requires matches to start at `lastIndex`, but a partial match failure resets `lastIndex` to 0. This means subsequent attempts cannot "continue" from where the previous match failed, making progressive character-by-character validation problematic.
164
-
165
- **Example:**
228
+ 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.
166
229
 
167
230
  ```javascript
168
- const pattern = /hello/y;
169
- const partial = createPartialMatchRegex(pattern);
231
+ import PartialMatchRegExp from "regex-partial-match";
232
+
233
+ const partial = new PartialMatchRegExp(/hello/y);
170
234
 
171
- pattern.lastIndex = 0;
172
- partial.test("h"); // succeeds, lastIndex advances
173
- partial.test("he"); // succeeds, but lastIndex was reset by previous test
174
- // Cannot reliably continue partial matching with sticky flag
235
+ partial.lastIndex = 2;
236
+ partial.test("xyhello"); // true — partial match at position 2
237
+ partial.test("xyworld"); // false — no match at position 2, no forward scan
238
+ partial.lastIndex = 2;
239
+ partial.test("xyhel"); // true — partial prefix "hel" at position 2
175
240
  ```
176
241
 
177
- **Recommendation:** Avoid using the `y` flag with partial matching unless you fully understand the implications.
242
+ **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:
178
243
 
179
- ### Global Flag (`g`)
244
+ ```javascript
245
+ const partial = new PartialMatchRegExp(/hello/y);
180
246
 
181
- The **global flag is preserved** but may not be necessary for partial matching use cases. The `g` flag affects behavior when using `.exec()` repeatedly to find all matches, but partial matching typically validates a single prefix at a time.
247
+ partial.test("h"); // true, lastIndex → 1
248
+ partial.test("he"); // false — sticky requires a match at position 1 of "he",
249
+ // but "e" is not a valid start of the pattern
250
+ partial.test("hel"); // true (lastIndex was reset to 0 by the previous failure)
251
+ ```
252
+
253
+ 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.
182
254
 
183
- The global flag does not cause issues like the sticky flag, as partial patterns naturally match from the beginning of the input. However, if you're using `lastIndex` to track position, be aware that failed matches will reset it to 0.
255
+ 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).
184
256
 
185
257
  ### "String properties"
186
258
 
@@ -190,13 +262,15 @@ Hence, `[\p{RGI_Emoji_Flag_Sequence}]` will match `🇺🇳` as a whole, but not
190
262
 
191
263
  In [`v` mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) expressions, where `[\q{abc}]` syntax is used in isolation (rather than its canonical use-case as a subtraction/intersection of another character class), this will also only match entirely or not at all. i.e. `abc` can match, but not partially.
192
264
 
193
- ## Examples
265
+ ## 💡 Examples
194
266
 
195
267
  ### Form Validation
196
268
 
197
269
  ```javascript
270
+ import PartialMatchRegExp from "regex-partial-match";
271
+
198
272
  const emailPattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
199
- const partial = createPartialMatchRegex(emailPattern);
273
+ const partial = new PartialMatchRegExp(emailPattern);
200
274
 
201
275
  function validateEmail(input) {
202
276
  return partial.test(input) ? "valid" : "invalid";
@@ -212,8 +286,10 @@ validateEmail("@@invalid"); // 'invalid' - cannot match
212
286
  ### Autocomplete
213
287
 
214
288
  ```javascript
289
+ import PartialMatchRegExp from "regex-partial-match";
290
+
215
291
  const commandPattern = /^(help|quit|save|load)/;
216
- const partial = createPartialMatchRegex(commandPattern);
292
+ const partial = new PartialMatchRegExp(commandPattern);
217
293
 
218
294
  function getSuggestions(input) {
219
295
  return partial.test(input) ? "valid prefix" : "no suggestions";
@@ -228,9 +304,11 @@ getSuggestions("xyz"); // 'no suggestions'
228
304
  ### Stream Processing
229
305
 
230
306
  ```javascript
307
+ import PartialMatchRegExp from "regex-partial-match";
308
+
231
309
  // Process streaming data with pattern matching at chunk boundaries
232
310
  const pattern = /\{"[^"]+":"[^"]+"\}/; // Match JSON objects
233
- const partial = createPartialMatchRegex(pattern);
311
+ const partial = new PartialMatchRegExp(pattern);
234
312
  let buffer = "";
235
313
 
236
314
  function processChunk(chunk) {
@@ -261,49 +339,108 @@ processChunk("invalid{"); // [] - discarded, buffer: ''
261
339
 
262
340
  Useful for parsing log files, network streams, or any chunked data where records may be split across boundaries.
263
341
 
264
- ## API
342
+ ## 🔌 API
265
343
 
266
- ### `createPartialMatchRegex(regex: RegExp): RegExp`
344
+ ### `new PartialMatchRegExp(pattern: RegExp | string, flags?: string)`
267
345
 
268
- Transforms a regular expression to support partial matching.
346
+ 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.
269
347
 
270
348
  Available via the default entry point of the package.
271
349
 
272
350
  **Parameters:**
273
351
 
274
- - `regex` - The regular expression to transform
352
+ - `pattern` - A `RegExp` instance, or a pattern source string (as accepted by the `RegExp` constructor)
353
+ - `flags` - A flags string, used only when `pattern` is a string (as accepted by the `RegExp` constructor)
275
354
 
276
355
  **Returns:**
277
356
 
278
- - A new `RegExp` that matches partial strings
357
+ - A `PartialMatchRegExp` instance that matches partial strings of the original pattern
279
358
 
280
- ### `RegExp.prototype.toPartialMatchRegex(): RegExp`
359
+ ### `RegExp.prototype.toPartialMatchRegex(): PartialMatchRegExp`
281
360
 
282
361
  When using `import 'regex-partial-match/extend'`, this method is added to `RegExp.prototype`.
283
362
 
284
363
  **Returns:**
285
364
 
286
- - A new `RegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
365
+ - A new `PartialMatchRegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
287
366
 
288
- ## License
367
+ ### `PartialMatchRegExp.prototype.features: ReadonlySet<RegexFeature>`
368
+
369
+ Building the partial-match regex requires walking the entire source pattern once. As a side effect of that same walk, each instance records which syntactic constructs its pattern actually uses, exposed as a `features` set — no separate scan of the source is performed to produce it.
370
+
371
+ This is useful for consumers building on top of `PartialMatchRegExp` who need to reason about which constructs a *specific* pattern uses, without writing their own regex parser to find out. Two concrete cases:
372
+
373
+ - **Flagging patterns likely to hit one of the [caveats](#caveats) documented above.** For example, a pattern combining `backreference` with `lookbehind`, `negativeLookahead`, or `negativeLookbehind` is a candidate for the [atomic-backreference caveat](#backreferences); one combining `backreference` with `disjunction` is a candidate for the [prefix-ambiguous top-level alternation caveat](#prefix-ambiguous-top-level-alternation). A consumer accepting user-supplied patterns can surface a warning instead of letting the edge case surprise someone later.
374
+ - **Restricting which constructs a product surface allows.** e.g. a system that only wants to accept "simple" patterns (no lookaround, no backreferences) from untrusted input can check `features` against an allow-list and reject the rest, without needing to hand-roll that check against the raw pattern source.
375
+
376
+ ```javascript
377
+ import PartialMatchRegExp from "regex-partial-match";
378
+
379
+ const partial = new PartialMatchRegExp(/^[a-z]+(?<domain>\.[a-z]+)\1/);
380
+
381
+ partial.features; // Set { "startAnchor", "characterClass", "quantifier", "namedGroup", "capturingGroup", "backreference" }
382
+ partial.features.has("backreference"); // true
383
+ ```
384
+
385
+ `RegexFeature` is a string union, exported alongside `PartialMatchRegExp`:
386
+
387
+ | Feature | Matches | Notes |
388
+ | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
389
+ | `patternCharacter` | An ordinary literal character | |
390
+ | `startAnchor` | Top-level `^` | |
391
+ | `endAnchor` | Top-level `$` | |
392
+ | `wordBoundary` | Top-level `\b` | |
393
+ | `nonWordBoundary` | Top-level `\B` | |
394
+ | `lookahead` | `(?=...)` | |
395
+ | `negativeLookahead` | `(?!...)` | |
396
+ | `lookbehind` | `(?<=...)` | |
397
+ | `negativeLookbehind` | `(?<!...)` | |
398
+ | `backreference` | `\1` | |
399
+ | `namedBackreference` | `\k<name>` | |
400
+ | `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
401
+ | `capturingGroup` | `(...)`, including named groups | |
402
+ | `nonCapturingGroup` | `(?:...)` | |
403
+ | `modifierGroup` | `(?ims:...)` | |
404
+ | `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
405
+ | `characterClass` | `[...]` | |
406
+ | `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
407
+ | `classIntersection` | `&&` inside a character class | `v` flag only |
408
+ | `classSubtraction` | `--` inside a character class | `v` flag only |
409
+ | `disjunction` | `\|` | |
410
+ | `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
411
+ | `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only — otherwise tagged `otherEscape` |
412
+ | `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
413
+ | `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
414
+ | `controlLetterEscape` | `\cX` | |
415
+ | `hexEscapeSequence` | `\xXX` | |
416
+ | `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
417
+ | `otherEscape` | Any other `\X`, e.g. `\.`, `\0` | |
418
+
419
+ Two things worth knowing about how these tags line up with the grammar:
420
+
421
+ - **One ECMA-262 production can map to several tags.** `Assertion` alone covers `^`, `$`, `\b`, `\B`, and all four lookarounds — `features` splits it by whichever discriminant is easiest to read off during the walk (`^` vs `$`, `=` vs `!` after `(?<`, etc.), since that information is free at the point each construct is recognized.
422
+ - **A named capturing group always carries both `namedGroup` and `capturingGroup`.** The grammar treats a capturing group with a name and one without as the same production (`( GroupSpecifier? Disjunction )`), not two, so both tags are added together.
423
+
424
+ ## 📜 License
289
425
 
290
426
  ISC License - see [LICENSE](./LICENSE) file for details.
291
427
 
292
- ## Credits
428
+ ## 🙌 Credits
293
429
 
294
430
  Algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski).
295
431
 
296
- ## Contributing
432
+ ## 🤝 Contributing
297
433
 
298
434
  Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/TomStrepsil/regex-partial-match).
299
435
 
300
- ## Related projects
301
-
302
- | Project | Description |
303
- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
304
- | [`incr-regex-package`](https://www.npmjs.com/package/incr-regex-package) | Incremental regex matcher |
305
- | [`dfa`](https://github.com/foliojs/dfa) | Compiles a regular expression like syntax to fast deterministic finite automata, which could be used to partial match? |
306
- | [`refa`](https://github.com/RunDevelopment/refa) | Can [convert regular expressions to an Abstract Syntax Tree](https://rundevelopment.github.io/refa/docs/latest/classes/JS.Parser.html), which might afford partial-match capability? |
307
- | [`@eslint-community/regexpp`](https://github.com/eslint-community/regexpp) | A regular expression parser for ECMAScript with AST generation and visitor implementation |
308
- | [`Regex+`](https://www.npmjs.com/package/regex) | template literal, transforming native regular expressions |
309
- | [`Awesome Regex`](https://github.com/slevithan/awesome-regex) | Curated list of tools, tutorials, libraries, and other resources, covering all major regex flavours |
436
+ ## 🔗 Related projects
437
+
438
+ | Project | Description |
439
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
440
+ | [`incr-regex-package`](https://www.npmjs.com/package/incr-regex-package) | Incremental regex matcher |
441
+ | [`dfa`](https://github.com/foliojs/dfa) | Compiles a regular expression like syntax to fast deterministic finite automata, which could be used to partial match? |
442
+ | [`refa`](https://github.com/RunDevelopment/refa) | Can [convert regular expressions to an Abstract Syntax Tree](https://rundevelopment.github.io/refa/docs/latest/classes/JS.Parser.html), which might afford partial-match capability? |
443
+ | [`@eslint-community/regexpp`](https://github.com/eslint-community/regexpp) | A regular expression parser for ECMAScript with AST generation and visitor implementation |
444
+ | [`Regex+`](https://www.npmjs.com/package/regex) | template literal, transforming native regular expressions |
445
+ | [`Awesome Regex`](https://github.com/slevithan/awesome-regex) | Curated list of tools, tutorials, libraries, and other resources, covering all major regex flavours |
446
+ | [`replace-content-transformer`](https://github.com/TomStrepsil/replace-content-transformer) | A toolkit for stream content replacement, underpinned by `regex-partial-match` |
@@ -0,0 +1,16 @@
1
+ export type RegexFeature = "patternCharacter" | "startAnchor" | "endAnchor" | "wordBoundary" | "nonWordBoundary" | "lookahead" | "negativeLookahead" | "lookbehind" | "negativeLookbehind" | "backreference" | "namedBackreference" | "namedGroup" | "capturingGroup" | "nonCapturingGroup" | "modifierGroup" | "modifierGroupWithRemoval" | "characterClass" | "nestedCharacterClass" | "classIntersection" | "classSubtraction" | "disjunction" | "quantifier" | "unicodePropertyEscape" | "characterClassEscape" | "controlEscape" | "controlLetterEscape" | "hexEscapeSequence" | "unicodeEscapeSequence" | "otherEscape";
2
+ export interface DynamicPath {
3
+ originalCaptureScan: RegExp;
4
+ preScan: RegExp;
5
+ expand: (capture: RegExpExecArray) => string;
6
+ }
7
+ export type CompiledPartial = ({
8
+ kind: "static";
9
+ regex: RegExp;
10
+ } | {
11
+ kind: "dynamic";
12
+ dynamic: DynamicPath;
13
+ }) & {
14
+ features: Set<RegexFeature>;
15
+ };
16
+ export declare const compilePartial: (regex: RegExp) => CompiledPartial;