regex-partial-match 0.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright 2025 Tom Pereira (https://github.com/TomStrepsil)
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # regex-partial-match
2
+
3
+ A zero-dependency regular expression transform for partial matching, enabling validation of incomplete input strings against regex patterns.
4
+
5
+ ## Problem statement
6
+
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
+
9
+ ## Overview
10
+
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
+
13
+ **Based on an algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski)**, re-created for NPM via ISC license, with permission.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install regex-partial-match
19
+ ```
20
+
21
+ If your runtime doesn't support these features, attempting to use patterns with them will throw a `SyntaxError`.
22
+
23
+ ## Usage
24
+
25
+ ### Basic Usage
26
+
27
+ ```javascript
28
+ import createPartialMatchRegex from "regex-partial-match";
29
+
30
+ const pattern = /hello world/;
31
+ const partial = createPartialMatchRegex(pattern);
32
+
33
+ partial.test("h"); // true - could match
34
+ partial.test("hello"); // true - could match
35
+ partial.test("hello world"); // true - full match
36
+ partial.test("goodbye"); // false - cannot match
37
+ ```
38
+
39
+ ### Extending RegExp.prototype
40
+
41
+ ```javascript
42
+ import "regex-partial-match/extend";
43
+
44
+ const partial = /hello world/.createPartialMatchRegexRegex();
45
+
46
+ partial.test("hel"); // true
47
+ ```
48
+
49
+ ## How It Works
50
+
51
+ The library transforms a regular expression by wrapping each atomic element in a non-capturing group with an alternation to end-of-input (`$`):
52
+
53
+ ```javascript
54
+ /abc/ → /(?:a|$)(?:b|$)(?:c|$)/
55
+ ```
56
+
57
+ This allows the pattern to match prefixes of the original pattern, enabling validation of incomplete input.
58
+
59
+ 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.
60
+
61
+ The library has been stress-tested with various regular expression features in isolation, and some in likely combination, but obviously its an unbounded test space, and syntactically valid regular expressions nevertheless support contradictory patterns e.g.
62
+
63
+ - `/\b\B/` - impossible to match both a word boundary and a non-word boundary
64
+ - `/$^/` - end cannot come before start
65
+ - `x{2}?` - lazy quantifiers are mutually exclusive to fixed-length assertions
66
+
67
+ Such combinations have not been tested.
68
+
69
+ [^1]: To remain lightweight, no runtime type validation is applied, so non-typescript consumers will be reliant on underlying errors thrown, if used incorrectly.
70
+
71
+ ## Supported Features
72
+
73
+ - 🔤 [Literal characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Literal_character)
74
+ - 📋 [Character classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class) (`[abc]`, `[^abc]`, `[a-z]`)
75
+ - 🔣 [Character escapes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape) (`\n`, `\t`, `\x61`, `\u0061`, `\u{1F600}`)
76
+ - 🌐 [Unicode character class escape](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) (`\p{Letter}`, `\P{Letter}`)
77
+ - 🔢 [Quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Quantifier) (`*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`)
78
+ - 🔀 [Disjunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction) (`a|b`)
79
+ - 👥 [Groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Capturing_group) (capturing and non-capturing) (`(?:abc)`, `(abc)`, `(?<named>abc)`)
80
+ - 👀 [Lookahead assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookahead_assertion) (`(?=...)`, `(?!...)`)
81
+ - 👈 [Lookbehind assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`)
82
+ - ⚓ [Anchors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Input_boundary_assertion) (`^`, `$`)
83
+ - 🏴 [Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags): `g`, `i`, `m`, `s`, `u`, `d`, `y` (See caveats for `y`)
84
+
85
+ ## Unsupported Features
86
+
87
+ The following regex features are **not currently supported**:
88
+
89
+ - ⚠️ [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).
90
+ - ❌ [Unicode sets (`v` flag)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) - ES2024+
91
+ - ❌ [Modifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`) - ES2025+
92
+
93
+ ## Browser Compatibility
94
+
95
+ The library is compiled to **ES5** for broad compatibility with older browsers and JavaScript environments. However, certain regular expression features naturally require ES2015+ support:
96
+
97
+ - [**Unicode escapes**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) (`\u{...}`) - ES2015+
98
+ - [**Unicode property escapes**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) (`\p{...}`, `\P{...}`) - ES2018+
99
+ - [**Lookbehind assertions**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`) - ES2018+
100
+ - [**Named capturing groups**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) (`(?<name>...)`) - ES2018+
101
+ - [**`s` (dotAll) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll) - ES2018+
102
+ - [**`d` (hasIndices) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/hasIndices) - ES2022+
103
+
104
+ ## Caveats
105
+
106
+ ### Backreferences
107
+
108
+ **Backreferences with variable-length captures cannot be partially matched** due to ambiguity in how the pattern should be matched. While simple fixed-length backreferences like `/(abc)\1/` could theoretically be transformed (if accepting that the group indexes would be polluted, compared to the original), to become `/(?:(a)|$)(?:(b)|$)(?:(c)|$)(?:\1|$)(?:\2|$)(?:\3|$)/` variable-length patterns break down.
109
+
110
+ For example, with `/(\w+) \1/` (matches "hello hello"):
111
+
112
+ - A naive transformation `/(?:(\w+)|$)(?:( )|$)(?:\1|$)/` creates ambiguity
113
+ - For input "hello h", the regex can match in multiple ways:
114
+ - Capture group 1 as "hello", skip the space with `$`, skip `\1` with `$` ✓
115
+ - Capture group 1 as "h", skip the space with `$`, match `\1` as "h" ✓ (at position 6)
116
+ - The engine chooses the second interpretation, matching just "h" instead of recognizing "hello h" as a partial match
117
+
118
+ The core issue: **the regex engine cannot determine which part of the input should be captured vs. which part should match the backreference** when dealing with variable-length patterns and incomplete input. Fixed-length patterns like `/(abc)\1/` avoid this ambiguity since each character position has a clear role.
119
+
120
+ ### Surrogate Pair Matching
121
+
122
+ In unicode-aware mode (`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.
123
+
124
+ ### [Sticky](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) Flag (`y`)
125
+
126
+ 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.
127
+
128
+ **Example:**
129
+
130
+ ```javascript
131
+ const pattern = /hello/y;
132
+ const partial = createPartialMatchRegex(pattern);
133
+
134
+ pattern.lastIndex = 0;
135
+ partial.test("h"); // succeeds, lastIndex advances
136
+ partial.test("he"); // succeeds, but lastIndex was reset by previous test
137
+ // Cannot reliably continue partial matching with sticky flag
138
+ ```
139
+
140
+ **Recommendation:** Avoid using the `y` flag with partial matching unless you fully understand the implications.
141
+
142
+ ### Global Flag (`g`)
143
+
144
+ 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.
145
+
146
+ 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.
147
+
148
+ ## Examples
149
+
150
+ ### Form Validation
151
+
152
+ ```javascript
153
+ import createPartialMatchRegex from "regex-partial-match";
154
+
155
+ const emailPattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
156
+ const partial = createPartialMatchRegex(emailPattern);
157
+
158
+ function validateEmail(input) {
159
+ return partial.test(input) ? "valid" : "invalid";
160
+ }
161
+
162
+ validateEmail("user"); // 'valid' - could become valid
163
+ validateEmail("user@"); // 'valid' - could become valid
164
+ validateEmail("user@example"); // 'valid' - could become valid
165
+ validateEmail("user@example.com"); // 'valid' - complete match
166
+ validateEmail("@@invalid"); // 'invalid' - cannot match
167
+ ```
168
+
169
+ ### Autocomplete
170
+
171
+ ```javascript
172
+ const commandPattern = /^(help|quit|save|load)/;
173
+ const partial = createPartialMatchRegex(commandPattern);
174
+
175
+ function getSuggestions(input) {
176
+ return partial.test(input) ? "valid prefix" : "no suggestions";
177
+ }
178
+
179
+ getSuggestions("h"); // 'valid prefix'
180
+ getSuggestions("hel"); // 'valid prefix'
181
+ getSuggestions("help"); // 'valid prefix'
182
+ getSuggestions("xyz"); // 'no suggestions'
183
+ ```
184
+
185
+ ### Stream Processing
186
+
187
+ ```javascript
188
+ import createPartialMatchRegex from "regex-partial-match";
189
+
190
+ // Process streaming data with pattern matching at chunk boundaries
191
+ const pattern = /\{"[^"]+":"[^"]+"\}/; // Match JSON objects
192
+ const partial = createPartialMatchRegex(pattern);
193
+ let buffer = "";
194
+
195
+ function processChunk(chunk) {
196
+ buffer += chunk;
197
+ const matches = [];
198
+
199
+ // Extract complete matches
200
+ let match;
201
+ while ((match = pattern.exec(buffer))) {
202
+ matches.push(match[0]);
203
+ buffer = buffer.slice(match.index + match[0].length);
204
+ }
205
+
206
+ // Discard buffer if it cannot possibly complete
207
+ if (buffer && !partial.test(buffer)) {
208
+ buffer = "";
209
+ }
210
+
211
+ return matches;
212
+ }
213
+
214
+ processChunk('{"na'); // [] - partial, buffer: '{"na'
215
+ processChunk('me":"Jo'); // [] - partial, buffer: '{"name":"Jo'
216
+ processChunk('hn"}{"age":'); // ['{"name":"John"}'] - buffer: '{"age":'
217
+ processChunk("25}"); // ['{"age":25}'] - buffer: ''
218
+ processChunk("invalid{"); // [] - discarded, buffer: ''
219
+ ```
220
+
221
+ Useful for parsing log files, network streams, or any chunked data where records may be split across boundaries.
222
+
223
+ ## API
224
+
225
+ ### `createPartialMatchRegex(regex: RegExp): RegExp`
226
+
227
+ Transforms a regular expression to support partial matching.
228
+
229
+ Available via the default entry point of the package.
230
+
231
+ **Parameters:**
232
+
233
+ - `regex` - The regular expression to transform
234
+
235
+ **Returns:**
236
+
237
+ - A new `RegExp` that matches partial strings
238
+
239
+ ### `RegExp.prototype.toPartialMatchRegex(): RegExp`
240
+
241
+ When using `import 'regex-partial-match/extend'`, this method is added to `RegExp.prototype`.
242
+
243
+ **Returns:**
244
+
245
+ - A new `RegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
246
+
247
+ ## License
248
+
249
+ ISC License - see [LICENSE](./LICENSE) file for details.
250
+
251
+ ## Credits
252
+
253
+ Algorithm created by [Lucas Trzesniewski](https://github.com/ltrzesniewski).
254
+
255
+ ## Contributing
256
+
257
+ Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/TomStrepsil/regex-partial-match).
258
+
259
+ ## Related packages
260
+
261
+ - [`incr-regex-package`](https://www.npmjs.com/package/incr-regex-package)
262
+ - Incremental regex matcher
263
+ - [`dfa`](https://github.com/foliojs/dfa)
264
+ - Compiles a regular expression like syntax to fast deterministic finite automata, which could be used to partial match?
265
+ - [`refa`](https://github.com/RunDevelopment/refa)
266
+ - 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?
@@ -0,0 +1,6 @@
1
+ declare global {
2
+ interface RegExp {
3
+ toPartialMatchRegex(): RegExp;
4
+ }
5
+ }
6
+ export {};
package/lib/extend.js ADDED
@@ -0,0 +1,4 @@
1
+ import toPartialMatch from "./index.js";
2
+ RegExp.prototype.toPartialMatchRegex = function () {
3
+ return toPartialMatch(this);
4
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare const createPartialMatchRegex: (regex: RegExp) => RegExp;
2
+ export default createPartialMatchRegex;
package/lib/index.js ADDED
@@ -0,0 +1,133 @@
1
+ var characterClassGlobalRegex = /\[(?:\\.|.)*?\]/g;
2
+ var occurrencesRegex = /\{\d+,?\d*\}/g;
3
+ var createPartialMatchRegex = function (regex) {
4
+ var source = regex.source;
5
+ var i = 0;
6
+ function extractSubstring(length) {
7
+ return source.substring(i, (i += length));
8
+ }
9
+ function process() {
10
+ var result = "";
11
+ function appendOptional(length) {
12
+ result += "(?:" + extractSubstring(length) + "|$)";
13
+ }
14
+ function appendRaw(length) {
15
+ result += extractSubstring(length);
16
+ }
17
+ while (i < source.length) {
18
+ switch (source[i]) {
19
+ case "\\":
20
+ switch (source[i + 1]) {
21
+ case "c":
22
+ appendOptional(3);
23
+ break;
24
+ case "k":
25
+ appendOptional(source.indexOf(">", i) - i + 1);
26
+ break;
27
+ case "u":
28
+ if (regex.unicode && source[i + 2] === "{") {
29
+ appendOptional(source.indexOf("}", i) - i + 1);
30
+ }
31
+ else {
32
+ appendOptional(6);
33
+ }
34
+ break;
35
+ case "p":
36
+ case "P":
37
+ if (regex.unicode) {
38
+ appendOptional(source.indexOf("}", i) - i + 1);
39
+ }
40
+ else {
41
+ appendOptional(2);
42
+ }
43
+ break;
44
+ case "x":
45
+ appendOptional(4);
46
+ break;
47
+ default:
48
+ appendOptional(2);
49
+ break;
50
+ }
51
+ break;
52
+ case "[": {
53
+ characterClassGlobalRegex.lastIndex = i;
54
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- must match, since otherwise an invalid regex
55
+ var match = characterClassGlobalRegex.exec(source)[0];
56
+ appendOptional(match.length);
57
+ break;
58
+ }
59
+ case "|":
60
+ case "^":
61
+ case "*":
62
+ case "+":
63
+ case "?":
64
+ case "$":
65
+ appendRaw(1);
66
+ break;
67
+ case "{": {
68
+ occurrencesRegex.lastIndex = i;
69
+ var regExpExecArray = occurrencesRegex.exec(source);
70
+ if (regExpExecArray) {
71
+ appendRaw(regExpExecArray[0].length);
72
+ }
73
+ else {
74
+ appendOptional(1);
75
+ }
76
+ break;
77
+ }
78
+ case "(":
79
+ if (source[i + 1] == "?") {
80
+ switch (source[i + 2]) {
81
+ case ":":
82
+ result += "(?:";
83
+ i += 3;
84
+ result += process() + "|$)";
85
+ break;
86
+ case "=":
87
+ result += "(?=";
88
+ i += 3;
89
+ result += process() + ")";
90
+ break;
91
+ case "!": {
92
+ var temp = i;
93
+ i += 3;
94
+ process();
95
+ result += source.substring(temp, i);
96
+ break;
97
+ }
98
+ case "<":
99
+ switch (source[i + 3]) {
100
+ case "=":
101
+ case "!": {
102
+ var temp = i;
103
+ i += 4;
104
+ process();
105
+ result += source.substring(temp, i);
106
+ break;
107
+ }
108
+ default:
109
+ appendRaw(source.indexOf(">", i) - i + 1);
110
+ result += process() + "|$)";
111
+ break;
112
+ }
113
+ break;
114
+ }
115
+ }
116
+ else {
117
+ appendRaw(1);
118
+ result += process() + "|$)";
119
+ }
120
+ break;
121
+ case ")":
122
+ ++i;
123
+ return result;
124
+ default:
125
+ appendOptional(1);
126
+ break;
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+ return new RegExp(process(), regex.flags);
132
+ };
133
+ export default createPartialMatchRegex;
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "regex-partial-match",
3
+ "version": "0.1.0",
4
+ "description": "A regular expression transform for partial matching",
5
+ "engines": {
6
+ "node": ">=23"
7
+ },
8
+ "keywords": [
9
+ "regexp",
10
+ "regular expressions",
11
+ "partial matcher"
12
+ ],
13
+ "license": "ISC",
14
+ "author": {
15
+ "name": "Tom Pereira",
16
+ "url": "https://github.com/TomStrepsil"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/TomStrepsil/regex-partial-match/issues"
20
+ },
21
+ "homepage": "https://github.com/TomStrepsil/regex-partial-match#readme",
22
+ "contributors": [
23
+ {
24
+ "name": "Lucas Trzesniewski",
25
+ "url": "https://github.com/ltrzesniewski"
26
+ }
27
+ ],
28
+ "type": "module",
29
+ "main": "lib/index.js",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./lib/index.js",
33
+ "types": "./lib/index.d.ts"
34
+ },
35
+ "./extend": {
36
+ "import": "./lib/extend.js",
37
+ "types": "./lib/extend.d.ts"
38
+ }
39
+ },
40
+ "files": [
41
+ "lib"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/TomStrepsil/regex-partial-match.git"
46
+ },
47
+ "sideEffects": false,
48
+ "scripts": {
49
+ "lint": "eslint .",
50
+ "lint:fix": "eslint . --fix",
51
+ "prepare": "simple-git-hooks",
52
+ "prepublishOnly": "tsc -p tsconfig.build.json",
53
+ "test": "vitest run --config ./test/vitest.config.ts",
54
+ "test:watch": "vitest --config ./test/vitest.config.ts"
55
+ },
56
+ "simple-git-hooks": {
57
+ "pre-commit": "node_modules/.bin/lint-staged"
58
+ },
59
+ "lint-staged": {
60
+ "*.{ts,js}": [
61
+ "npm run lint:fix"
62
+ ]
63
+ },
64
+ "publishConfig": {
65
+ "registry": "https://registry.npmjs.org"
66
+ },
67
+ "devDependencies": {
68
+ "@eslint/js": "^9.17.0",
69
+ "eslint": "^9.17.0",
70
+ "jiti": "^2.6.1",
71
+ "lint-staged": "^15.2.11",
72
+ "simple-git-hooks": "^2.12.1",
73
+ "typescript": "^5.9.3",
74
+ "typescript-eslint": "^8.18.1",
75
+ "vitest": "^4.0.15"
76
+ }
77
+ }