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.
Files changed (137) hide show
  1. package/README.md +154 -301
  2. package/lib/core/corePartialMatchRegExp.d.ts +24 -0
  3. package/lib/core/corePartialMatchRegExp.js +22 -0
  4. package/lib/core/index.d.ts +4 -0
  5. package/lib/core/index.js +2 -0
  6. package/lib/extend/index.d.ts +6 -7
  7. package/lib/extend/index.js +1 -1
  8. package/lib/features/index.d.ts +20 -0
  9. package/lib/features/index.js +23 -0
  10. package/lib/hitEnd/index.d.ts +45 -0
  11. package/lib/hitEnd/index.js +57 -0
  12. package/lib/hitEnd/matchHitEnd.d.ts +4 -0
  13. package/lib/hitEnd/matchHitEnd.js +51 -0
  14. package/lib/{partialMatchRegExp/isComplete → hitEnd}/partRole.d.ts +1 -1
  15. package/lib/hitEnd/partRole.js +17 -0
  16. package/lib/hitEnd/truncationProbe.d.ts +8 -0
  17. package/lib/hitEnd/truncationProbe.js +187 -0
  18. package/lib/hitEnd/truncationProbeCache.d.ts +14 -0
  19. package/lib/index.d.ts +4 -0
  20. package/lib/index.js +4 -0
  21. package/lib/modules/backreferences/asNativeAtom.d.ts +2 -0
  22. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/asNativeAtom.js +2 -2
  23. package/lib/modules/backreferences/asPreScanPart.d.ts +2 -0
  24. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/asPreScanPart.js +2 -2
  25. package/lib/modules/backreferences/backreferenceRecorder.d.ts +2 -0
  26. package/lib/modules/backreferences/backreferenceRecorder.js +38 -0
  27. package/lib/modules/backreferences/compileDynamic.d.ts +4 -0
  28. package/lib/modules/backreferences/compileDynamic.js +53 -0
  29. package/lib/modules/backreferences/compiledBackreferences.d.ts +9 -0
  30. package/lib/modules/backreferences/compiledBackreferences.js +9 -0
  31. package/lib/modules/backreferences/constants.d.ts +2 -0
  32. package/lib/modules/backreferences/constants.js +2 -0
  33. package/lib/modules/backreferences/execDynamic.d.ts +2 -0
  34. package/lib/modules/backreferences/execDynamic.js +43 -0
  35. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/flagsAtBackreference.d.ts +1 -1
  36. package/lib/{partialMatchRegExp → modules/backreferences}/groupName.js +1 -1
  37. package/lib/modules/backreferences/index.d.ts +10 -0
  38. package/lib/modules/backreferences/index.js +20 -0
  39. package/lib/modules/backreferences/longestBakedPrefixEndingInput.js +46 -0
  40. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/renderParts.d.ts +1 -1
  41. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/renderParts.js +1 -1
  42. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/resolvedFromScan.d.ts +1 -1
  43. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/resolvedFromScan.js +2 -2
  44. package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/startsWithUnderFlags.js +1 -1
  45. package/lib/modules/carets/appendMultilineCaret.d.ts +7 -0
  46. package/lib/modules/carets/appendMultilineCaret.js +181 -0
  47. package/lib/modules/carets/caretRecorder.d.ts +2 -0
  48. package/lib/modules/carets/caretRecorder.js +164 -0
  49. package/lib/modules/carets/caretSyntax.d.ts +3 -0
  50. package/lib/modules/carets/caretSyntax.js +11 -0
  51. package/lib/modules/carets/index.d.ts +9 -0
  52. package/lib/modules/carets/index.js +18 -0
  53. package/lib/modules/carets/lineTerminator.d.ts +1 -0
  54. package/lib/modules/carets/lineTerminator.js +31 -0
  55. package/lib/modules/carets/needsCaretRules.d.ts +1 -0
  56. package/lib/modules/carets/needsCaretRules.js +15 -0
  57. package/lib/modules/carets/quantifierBounds.d.ts +3 -0
  58. package/lib/modules/carets/quantifierBounds.js +18 -0
  59. package/lib/modules/index.d.ts +3 -0
  60. package/lib/modules/index.js +2 -0
  61. package/lib/partialMatchRegExp/atomSyntax.d.ts +14 -1
  62. package/lib/partialMatchRegExp/atomSyntax.js +24 -1
  63. package/lib/partialMatchRegExp/backreferenceExpansion.d.ts +1 -6
  64. package/lib/partialMatchRegExp/backreferenceHook.d.ts +11 -0
  65. package/lib/partialMatchRegExp/backreferenceHook.js +1 -0
  66. package/lib/partialMatchRegExp/caretHook.d.ts +17 -0
  67. package/lib/partialMatchRegExp/caretHook.js +5 -0
  68. package/lib/partialMatchRegExp/compilePartial/compileWith.d.ts +3 -0
  69. package/lib/partialMatchRegExp/compilePartial/compileWith.js +36 -0
  70. package/lib/partialMatchRegExp/compilePartial/compiled.d.ts +17 -17
  71. package/lib/partialMatchRegExp/compilePartial/compiled.js +8 -21
  72. package/lib/partialMatchRegExp/compilePartial/constants.d.ts +4 -0
  73. package/lib/partialMatchRegExp/compilePartial/constants.js +4 -0
  74. package/lib/partialMatchRegExp/compilePartial/groupShape.d.ts +4 -0
  75. package/lib/partialMatchRegExp/compilePartial/groupShape.js +9 -0
  76. package/lib/partialMatchRegExp/compilePartial/toStatic.d.ts +2 -2
  77. package/lib/partialMatchRegExp/compilePartial/toStatic.js +2 -2
  78. package/lib/partialMatchRegExp/constants.d.ts +5 -0
  79. package/lib/partialMatchRegExp/constants.js +5 -0
  80. package/lib/partialMatchRegExp/createPartialMatchRegExp.d.ts +3 -0
  81. package/lib/partialMatchRegExp/createPartialMatchRegExp.js +21 -0
  82. package/lib/partialMatchRegExp/execFrom.d.ts +1 -0
  83. package/lib/partialMatchRegExp/execFrom.js +4 -0
  84. package/lib/partialMatchRegExp/index.d.ts +49 -3
  85. package/lib/partialMatchRegExp/index.js +31 -3
  86. package/lib/partialMatchRegExp/moduleHooks.d.ts +14 -0
  87. package/lib/partialMatchRegExp/moduleHooks.js +15 -0
  88. package/lib/partialMatchRegExp/opaqueModule.d.ts +9 -0
  89. package/lib/partialMatchRegExp/opaqueModule.js +1 -0
  90. package/lib/partialMatchRegExp/part.d.ts +2 -11
  91. package/lib/partialMatchRegExp/partialMatchInternals.d.ts +2 -1
  92. package/lib/partialMatchRegExp/partialMatchInternals.js +3 -1
  93. package/lib/partialMatchRegExp/quantifier.d.ts +6 -0
  94. package/lib/partialMatchRegExp/quantifier.js +9 -0
  95. package/lib/partialMatchRegExp/rawLookaroundInfo.d.ts +11 -0
  96. package/lib/partialMatchRegExp/rawLookaroundInfo.js +1 -0
  97. package/lib/partialMatchRegExp/regexFeatures.d.ts +1 -5
  98. package/lib/partialMatchRegExp/regexFeatures.js +2 -8
  99. package/lib/partialMatchRegExp/scope.d.ts +12 -0
  100. package/lib/partialMatchRegExp/scope.js +45 -0
  101. package/lib/partialMatchRegExp/walk.d.ts +13 -5
  102. package/lib/partialMatchRegExp/walk.js +211 -165
  103. package/lib/partialMatchRegExp/withModules.d.ts +13 -0
  104. package/lib/partialMatchRegExp/withModules.js +27 -0
  105. package/package.json +39 -56
  106. package/lib/partialMatchRegExp/compilePartial/asNativeAtom.d.ts +0 -2
  107. package/lib/partialMatchRegExp/compilePartial/asOptionalAtom.d.ts +0 -1
  108. package/lib/partialMatchRegExp/compilePartial/asOptionalAtom.js +0 -4
  109. package/lib/partialMatchRegExp/compilePartial/asPreScanPart.d.ts +0 -2
  110. package/lib/partialMatchRegExp/compilePartial/index.d.ts +0 -2
  111. package/lib/partialMatchRegExp/compilePartial/index.js +0 -79
  112. package/lib/partialMatchRegExp/compilePartial/longestBakedPrefixEndingInput.js +0 -50
  113. package/lib/partialMatchRegExp/compilePartial/reclassifyLegacyEscapes.d.ts +0 -2
  114. package/lib/partialMatchRegExp/compilePartial/reclassifyLegacyEscapes.js +0 -17
  115. package/lib/partialMatchRegExp/compilePartial/spliceOriginalSource.d.ts +0 -2
  116. package/lib/partialMatchRegExp/compilePartial/spliceOriginalSource.js +0 -12
  117. package/lib/partialMatchRegExp/isComplete/index.d.ts +0 -38
  118. package/lib/partialMatchRegExp/isComplete/index.js +0 -41
  119. package/lib/partialMatchRegExp/isComplete/isMatchComplete.d.ts +0 -3
  120. package/lib/partialMatchRegExp/isComplete/isMatchComplete.js +0 -14
  121. package/lib/partialMatchRegExp/isComplete/partRole.js +0 -28
  122. package/lib/partialMatchRegExp/isComplete/truncationProbe.d.ts +0 -8
  123. package/lib/partialMatchRegExp/isComplete/truncationProbe.js +0 -105
  124. package/lib/partialMatchRegExp/isComplete/truncationProbeCache.d.ts +0 -4
  125. package/lib/partialMatchRegExp/partialMatchRegExp.d.ts +0 -55
  126. package/lib/partialMatchRegExp/partialMatchRegExp.js +0 -114
  127. package/lib/partialMatchRegExp/preferLongerCaptures.d.ts +0 -1
  128. package/lib/partialMatchRegExp/preferLongerCaptures.js +0 -30
  129. /package/lib/{partialMatchRegExp/isComplete → hitEnd}/truncationProbeCache.js +0 -0
  130. /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/caseFoldFlags.d.ts +0 -0
  131. /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/caseFoldFlags.js +0 -0
  132. /package/lib/{partialMatchRegExp → modules/backreferences}/escapeAtom.d.ts +0 -0
  133. /package/lib/{partialMatchRegExp → modules/backreferences}/escapeAtom.js +0 -0
  134. /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/flagsAtBackreference.js +0 -0
  135. /package/lib/{partialMatchRegExp → modules/backreferences}/groupName.d.ts +0 -0
  136. /package/lib/{partialMatchRegExp/compilePartial → modules/backreferences}/longestBakedPrefixEndingInput.d.ts +0 -0
  137. /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, 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.
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
- ### Telling a prefix from a complete match
39
+ ### Extending RegExp.prototype
40
+
41
+ ```javascript
42
+ import "regex-partial-match/extend";
40
43
 
41
- `test()` and `exec()` answer "could this match?", which is `true` for a prefix and for a complete match alike. [`isComplete()`](#iscompletepartial-partialmatchregexp-match-regexpexecarray-boolean) separates the two, testing the original pattern instead doesn't [^1], giving the three states progressive validation actually needs:
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, { isComplete } from "regex-partial-match";
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 isComplete(partial, match) ? "complete" : "incomplete";
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
- [^1]:
62
- 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 got there by truncation. The two agree almost always, but a truncation branch firing 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](#why-the-question-cant-be-answered-from-the-outside) for the case where they diverge.
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) and you have no use for `isComplete`, import `PartialMatchRegExp` from its own subpath instead of the default entry, to avoid pulling in `isComplete`'s code:
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
- ### Extending RegExp.prototype
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
- const partial = /^hello world/.toPartialMatchRegex();
92
+ ### The lean entry: `regex-partial-match/core`
78
93
 
79
- partial.test("hel"); // true
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
- ## ⚙️ How It Works
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
- 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])`[^2]):
101
+ Both are also named exports of `regex-partial-match/modules`.
85
102
 
86
103
  ```javascript
87
- /abc/ → /(?:a|$(?![\s\S]))(?:b|$(?![\s\S]))(?:c|$(?![\s\S]))/
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
- This allows the pattern to match prefixes of the original pattern, enabling validation of incomplete input.
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
- Since the library accepts only valid regular expressions [^3], this enables the algorithm to make lots of unguarded assumptions about the source of the expression.
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
- 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.
118
+ #### Choosing an entry point
95
119
 
96
- - `/\b\B/` - impossible to match both a word boundary and a non-word boundary
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
- Contradictory patterns are not comprehensively supported and may partially match inappropriately.
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
- > [!NOTE]
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
- ### Patterns with backreferences
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
- 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, but that native result only wins outright if nothing earlier in the input could still be a viable partial — a cheap bound check settles that without needing to resolve the backreference's actual value, so the common case (no earlier partial exists) stays fast. 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.
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
- 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.
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](#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 *original* pattern uses the feature — everything else, including construction, `exec()` and `test()`, holds to the ES2015 floor. [`isComplete()`](#iscompletepartial-partialmatchregexp-match-regexpexecarray-boolean) is the one exception: it always requires **ES2018+**, regardless of the pattern, since its internal probe uses named capturing groups.
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
- ### [`.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)
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
- e.g.
197
-
198
- ```js
199
- /(?:x|$(?![\s\S]))/.exec("a"); // ['', index: 1, input: "a", groups: undefined];
200
- "a".match(/(?:x|$(?![\s\S]))/); // ['', index: 1, input: "a", groups: undefined];
201
- ```
202
-
203
- > [!TIP]
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(pattern: RegExp | string, flags?: string)`
275
+ ### `new PartialMatchRegExp()`
402
276
 
403
- 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.
277
+ ```typescript
278
+ new PartialMatchRegExp(pattern: RegExp | string, flags?: string)
279
+ ```
404
280
 
405
- 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).
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
- ### `RegExp.prototype.toPartialMatchRegex(): PartialMatchRegExp`
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
- ### `isComplete(partial: PartialMatchRegExp, match: RegExpExecArray): boolean`
306
+ ### `hitEnd()`
425
307
 
426
- Reports whether a match `partial` produced is a **match** of the original pattern, or merely a **prefix** of it. `exec()` alone cannot say: it returns the same shape of array for `"h"`, `"hello"` and `"hello world"` against `/hello world/`.
308
+ ```typescript
309
+ hitEnd(partial: PartialMatchRegExp, match: RegExpExecArray): boolean
310
+ ```
427
311
 
428
- Available as a named export of the default entry point: `import { isComplete } from 'regex-partial-match'`. Its probe requires ES2018+, so in an environment that doesn't tree-shake and has no use for it, import `PartialMatchRegExp` from `regex-partial-match/partialMatchRegExp` instead of the default entry to avoid bundling `isComplete`'s code; see [A note on Tree-Shaking](#a-note-on-tree-shaking).
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 every atom matched literally, so the path the match took is one the original pattern could have taken itself.
438
- - `false` when the match depended on the input running out — it took one of the `|$(?![\s\S])` branches described in [How It Works](#how-it-works). More input is needed, and the match's captures are **provisional**: a truncated path can capture values that no complete match ever produces.
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, { isComplete } from "regex-partial-match";
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
- prefix[0]; // 'hello'
447
- isComplete(partial, prefix); // false — a prefix, not a match
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 complete = partial.exec("hello world");
335
+ const greedy = new PartialMatchRegExp(/hello \w+/);
450
336
 
451
- complete[0]; // 'hello world'
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
- > Complete is not the same as *final*. A greedy pattern like `/hello \w+/` matches `"hello world"` completely, and would still match more of `"hello worldly"`.
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
- - **Checking whether the match ends at the end of input.** True for every prefix, but also true for the common case of a complete match of input as it is typed — `/^\d{4}/` against `"2024"` ends at end of input and is complete.
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
- `isComplete()` is necessary, but not sufficient, for a scanner whose output must be the same however the input is chunked. It cannot see a capture still growing *inside* an assertion: [`RepeatMatcher`](https://tc39.es/ecma262/#sec-runtime-semantics-repeatmatcher-abstract-operation) discards a repetition once it matches empty, so a truncation sentinel placed after a greedy repetition never gets a chance to fire there.
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
- match[1]; // 'b'
496
- isComplete(partial, match); // true
350
+ ### `features()`
497
351
 
498
- /a(?=(b+))/.exec("abbX")[1]; // 'bb' — the same capture, over more input
352
+ ```typescript
353
+ features(partial: PartialMatchRegExp): ReadonlySet<RegexFeature>
499
354
  ```
500
355
 
501
- This isn't a defect: every atom in the `"ab"` match matched literally, so `isComplete()` answers correctly by its own definition. It just doesn't mean *cannot change with more input* — the same distinction the [note above](#iscompletepartial-partialmatchregexp-match-regexpexecarray-boolean) draws for `/hello \w+/`, one level further in.
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
- ### `PartialMatchRegExp.prototype.features: ReadonlySet<RegexFeature>`
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
- 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.
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
- 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:
510
-
511
- - **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.
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.features; // Set { "startAnchor", "backreference", "namedGroup", "capturingGroup", "characterClass", "quantifier", "otherEscape" }
521
- partial.features.has("backreference"); // true
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 alongside `PartialMatchRegExp`:
525
-
526
- | Feature | Matches | Notes |
527
- | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
528
- | `patternCharacter` | An ordinary literal character | |
529
- | `startAnchor` | Top-level `^` | |
530
- | `endAnchor` | Top-level `$` | |
531
- | `wordBoundary` | Top-level `\b` | |
532
- | `nonWordBoundary` | Top-level `\B` | |
533
- | `lookahead` | `(?=...)` | |
534
- | `negativeLookahead` | `(?!...)` | |
535
- | `lookbehind` | `(?<=...)` | |
536
- | `negativeLookbehind` | `(?<!...)` | |
537
- | `backreference` | `\1` | |
538
- | `namedBackreference` | `\k<name>` | |
539
- | `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
540
- | `capturingGroup` | `(...)`, including named groups | |
541
- | `lookaroundCapture` | A capturing group inside any lookaround | Nesting, not a construct — always accompanied by `capturingGroup` |
542
- | `nonCapturingGroup` | `(?:...)` | |
543
- | `modifierGroup` | `(?ims:...)` | |
544
- | `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
545
- | `characterClass` | `[...]` | |
546
- | `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
547
- | `classIntersection` | `&&` inside a character class | `v` flag only |
548
- | `classSubtraction` | `--` inside a character class | `v` flag only |
549
- | `disjunction` | `\|` | |
550
- | `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
551
- | `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only — otherwise tagged `otherEscape` |
552
- | `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
553
- | `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
554
- | `controlLetterEscape` | `\cX` | |
555
- | `hexEscapeSequence` | `\xXX` | |
556
- | `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
557
- | `otherEscape` | Any other `\X`, e.g. `\.` | `\0` alone is tagged `otherEscape` only under `u`/`v`; otherwise `backreference`, [like any other digit escape](docs/backreferences.md) |
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.