regex-partial-match 1.3.0 → 2.0.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.
Files changed (137) hide show
  1. package/README.md +120 -267
  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,80 @@ state("2024-06"); // 'incomplete' - no error, keep typing
58
68
  state("2024-06-15"); // 'complete' - accept, enable submit
59
69
  ```
60
70
 
71
+ > [!NOTE]
72
+ > `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
+
61
76
  [^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.
77
+ Mimicking JDK's [`Matcher.hitEnd()`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--)
78
+
79
+ [^2]:
80
+ 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
81
 
64
82
  ### A note on Tree-Shaking
65
83
 
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:
84
+ If your environment doesn't tree-shake (e.g. Deno, or unbundled Node), the default entry loads `hitEnd` and `features` with the class. Import each from its own subpath instead, and only the ones you use:
67
85
 
68
86
  ```javascript
69
87
  import PartialMatchRegExp from "regex-partial-match/partialMatchRegExp";
88
+ import hitEnd from "regex-partial-match/hitEnd";
89
+ import features from "regex-partial-match/features";
70
90
  ```
71
91
 
72
- ### Extending RegExp.prototype
92
+ [`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).
73
93
 
74
- ```javascript
75
- import "regex-partial-match/extend";
94
+ ### The lean entry: `regex-partial-match/core`
76
95
 
77
- const partial = /^hello world/.toPartialMatchRegex();
96
+ `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`:
78
97
 
79
- partial.test("hel"); // true
80
- ```
81
-
82
- ## ⚙️ How It Works
98
+ | Module | Import | Supplies |
99
+ |---|---|---|
100
+ | `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 |
101
+ | `backreferences` | `regex-partial-match/modules/backreferences` | matching a backreference against the text its group captured |
83
102
 
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]):
103
+ Both are also named exports of `regex-partial-match/modules`.
85
104
 
86
105
  ```javascript
87
- /abc/ → /(?:a|$(?![\s\S]))(?:b|$(?![\s\S]))(?:c|$(?![\s\S]))/
106
+ import PartialMatchRegExp, { withModules } from "regex-partial-match/core";
107
+ import carets from "regex-partial-match/modules/carets";
108
+ import backreferences from "regex-partial-match/modules/backreferences";
109
+
110
+ new PartialMatchRegExp(/^\d{4}-\d{2}/).test("20"); // true
111
+
112
+ const WithBoth = withModules(carets, backreferences);
113
+ new WithBoth(/(a|b)\1/).test("a"); // true
88
114
  ```
89
115
 
90
- This allows the pattern to match prefixes of the original pattern, enabling validation of incomplete input.
116
+ `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
117
 
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.
118
+ 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
119
 
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.
120
+ #### Choosing an entry point
95
121
 
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
122
+ 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
123
 
102
- Contradictory patterns are not comprehensively supported and may partially match inappropriately.
124
+ | Error when constructed | Bind |
125
+ |---|---|
126
+ | `TypeError: Needs the carets module` | `carets` |
127
+ | `TypeError: Needs the backreferences module` | `backreferences` |
128
+ | `TypeError: Needs the carets and backreferences modules` | both |
103
129
 
104
- > [!NOTE]
105
- > See [Partial Match Parity](/docs/partial-match-parity.md) for full details on how the library compares to reference implementations
130
+ The error names every module the pattern needs, and none that is already bound.
106
131
 
107
- ### Patterns with backreferences
132
+ **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.
133
+
134
+ **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`.
135
+
136
+ ## ⚙️ How It Works
108
137
 
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.
138
+ 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:
110
139
 
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.
140
+ ```javascript
141
+ /abc/ → /(?:a|$(?![\s\S]))(?:b|$(?![\s\S]))(?:c|$(?![\s\S]))/
142
+ ```
112
143
 
144
+ 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
145
 
114
146
  ## ✅ Supported Features
115
147
 
@@ -122,19 +154,19 @@ Whatever that scan captures (or leaves `undefined`, if the group hasn't been rea
122
154
  - 🔢 [Quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Quantifier) (`*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`)
123
155
  - 🔀 [Disjunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction) (`a|b`)
124
156
  - 👥 [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)
157
+ - 🔙 [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
158
  - 👉 [Lookahead assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookahead_assertion) (`(?=...)`, `(?!...)`)
127
159
  - 👈 [Lookbehind assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion) (`(?<=...)`, `(?<!...)`)
128
160
  - ⚓ [Input Boundaries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Input_boundary_assertion) (`^`, `$`)
129
161
  - 🆒 [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`)
162
+ - 🏴 [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
163
  - 🎚️ [Modifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?im-s:...)`)
132
164
 
133
165
  ## 🚫 Unsupported Features
134
166
 
135
167
  The following regex features are **not currently supported**:
136
168
 
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).
169
+ - ⚠️ [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
170
 
139
171
  ## 🌐 Browser Compatibility
140
172
 
@@ -148,176 +180,20 @@ The library is compiled to **ES2015** (ECMAScript 6). Certain regular expression
148
180
  - [**`v` (unicodeSets) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) - ES2024+
149
181
  - [**Modifiers**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?i-ms:...)`) - ES2025+
150
182
 
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.
183
+ 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
184
 
153
185
  ## ⚠️ Caveats
154
186
 
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.
195
-
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:
187
+ See [Caveats](./docs/caveats.md) for details, examples and workarounds.
238
188
 
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.
189
+ - [**`.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)
190
+ - [**Backreferences**](./docs/caveats.md#backreferences): some backreferences stay atomic
191
+ - [Captures on a partial match](./docs/caveats.md#captures-on-a-partial-match)
192
+ - [Prefix-ambiguous top-level alternation](./docs/caveats.md#prefix-ambiguous-top-level-alternation)
193
+ - [**Positive lookbehinds**](./docs/caveats.md#positive-lookbehinds): must match in their entirety
194
+ - [**Surrogate pair matching**](./docs/caveats.md#surrogate-pair-matching): only whole astral characters match
195
+ - [**Sticky flag (`y`)**](./docs/caveats.md#sticky-flag-y): not suited to progressive input validation
196
+ - [**"String properties"**](./docs/caveats.md#string-properties): match atomically
321
197
 
322
198
  ## 💡 Examples
323
199
 
@@ -398,11 +274,15 @@ Useful for parsing log files, network streams, or any chunked data where records
398
274
 
399
275
  ## 🔌 API
400
276
 
401
- ### `new PartialMatchRegExp(pattern: RegExp | string, flags?: string)`
277
+ ### `new PartialMatchRegExp()`
402
278
 
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.
279
+ ```typescript
280
+ new PartialMatchRegExp(pattern: RegExp | string, flags?: string)
281
+ ```
282
+
283
+ 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.
404
284
 
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).
285
+ 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
286
 
407
287
  **Parameters:**
408
288
 
@@ -413,7 +293,11 @@ Available via the default entry point of the package, or via `regex-partial-matc
413
293
 
414
294
  - A `PartialMatchRegExp` instance that matches partial strings of the original pattern
415
295
 
416
- ### `RegExp.prototype.toPartialMatchRegex(): PartialMatchRegExp`
296
+ ### `toPartialMatchRegex()`
297
+
298
+ ```typescript
299
+ RegExp.prototype.toPartialMatchRegex(): PartialMatchRegExp
300
+ ```
417
301
 
418
302
  When using `import 'regex-partial-match/extend'`, this method is added to `RegExp.prototype`.
419
303
 
@@ -421,11 +305,15 @@ When using `import 'regex-partial-match/extend'`, this method is added to `RegEx
421
305
 
422
306
  - A new `PartialMatchRegExp` that matches partial strings, created from the `RegExp` instance the method was called on.
423
307
 
424
- ### `isComplete(partial: PartialMatchRegExp, match: RegExpExecArray): boolean`
308
+ ### `hitEnd()`
425
309
 
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/`.
310
+ ```typescript
311
+ hitEnd(partial: PartialMatchRegExp, match: RegExpExecArray): boolean
312
+ ```
313
+
314
+ 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/`.
427
315
 
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).
316
+ 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
317
 
430
318
  **Parameters:**
431
319
 
@@ -434,94 +322,59 @@ Available as a named export of the default entry point: `import { isComplete } f
434
322
 
435
323
  **Returns:**
436
324
 
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.
325
+ - `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).
326
+ - `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
327
 
440
328
  ```javascript
441
- import PartialMatchRegExp, { isComplete } from "regex-partial-match";
329
+ import PartialMatchRegExp, { hitEnd } from "regex-partial-match";
442
330
 
443
331
  const partial = new PartialMatchRegExp(/hello world/);
444
- const prefix = partial.exec("hello");
445
332
 
446
- prefix[0]; // 'hello'
447
- isComplete(partial, prefix); // false — a prefix, not a match
333
+ hitEnd(partial, partial.exec("hello")); // true — ran out of input inside "world"
334
+ hitEnd(partial, partial.exec("hello world!")); // false — settled short of the end
335
+ hitEnd(partial, partial.exec("hello world")); // false — consumed the last character without reading past it
448
336
 
449
- const complete = partial.exec("hello world");
337
+ const greedy = new PartialMatchRegExp(/hello \w+/);
450
338
 
451
- complete[0]; // 'hello world'
452
- isComplete(partial, complete); // true
339
+ hitEnd(greedy, greedy.exec("hello world")); // true — \w+ read the end looking for more
453
340
  ```
454
341
 
455
342
  > [!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
343
+ > Where the JDK is exact, `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.
459
344
 
460
- Neither of the obvious workarounds answers it:
461
-
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.
345
+ 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()` records a read of the end.
486
346
 
487
347
  #### What it cannot see
488
348
 
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");
349
+ - **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.
350
+ - **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
351
 
495
- match[1]; // 'b'
496
- isComplete(partial, match); // true
352
+ ### `features()`
497
353
 
498
- /a(?=(b+))/.exec("abbX")[1]; // 'bb' — the same capture, over more input
354
+ ```typescript
355
+ features(partial: PartialMatchRegExp): ReadonlySet<RegexFeature>
499
356
  ```
500
357
 
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.
358
+ Building the partial-match regex requires walking the entire source pattern, usually once. 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. 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.
502
359
 
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.
504
-
505
- ### `PartialMatchRegExp.prototype.features: ReadonlySet<RegexFeature>`
506
-
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
+ 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).
508
361
 
509
362
  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
363
 
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)/`.
364
+ - **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.
365
+ - **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.
366
+ - **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
367
 
515
368
  ```javascript
516
- import PartialMatchRegExp from "regex-partial-match";
369
+ import PartialMatchRegExp, { features } from "regex-partial-match";
517
370
 
518
371
  const partial = new PartialMatchRegExp(/^[a-z]+(?<domain>\.[a-z]+)\1/);
519
372
 
520
- partial.features; // Set { "startAnchor", "backreference", "namedGroup", "capturingGroup", "characterClass", "quantifier", "otherEscape" }
521
- partial.features.has("backreference"); // true
373
+ features(partial); // Set { "startAnchor", "backreference", "namedGroup", "capturingGroup", "characterClass", "quantifier", "otherEscape" }
374
+ features(partial).has("backreference"); // true
522
375
  ```
523
376
 
524
- `RegexFeature` is a string union, exported alongside `PartialMatchRegExp`:
377
+ `RegexFeature` is a string union, exported from `regex-partial-match` and `regex-partial-match/features`:
525
378
 
526
379
  | Feature | Matches | Notes |
527
380
  | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
@@ -554,7 +407,7 @@ partial.features.has("backreference"); // true
554
407
  | `controlLetterEscape` | `\cX` | |
555
408
  | `hexEscapeSequence` | `\xXX` | |
556
409
  | `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) |
410
+ | `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
411
 
559
412
  Three things worth knowing about how these tags line up with the grammar:
560
413
 
@@ -0,0 +1,24 @@
1
+ import type { PartialMatchRegExpConstructor } from "../partialMatchRegExp/index.ts";
2
+ /**
3
+ * `PartialMatchRegExp` without the rules that place a `^` in a group or after
4
+ * other parts under the `m` flag, or match a backreference, so that a bundle
5
+ * importing only `regex-partial-match/core` leaves them out.
6
+ *
7
+ * A pattern that needs either throws one `TypeError` naming every module it
8
+ * needs; every other pattern is transformed exactly as the class exported
9
+ * from `regex-partial-match` transforms it.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import PartialMatchRegExp from "regex-partial-match/core";
14
+ *
15
+ * new PartialMatchRegExp(/^hello world/).test("hello"); // true
16
+ * new PartialMatchRegExp(/(a)\1/); // throws TypeError
17
+ * ```
18
+ *
19
+ * @see {@link https://github.com/TomStrepsil/regex-partial-match#readme | Documentation}
20
+ */
21
+ declare const PartialMatchRegExp: PartialMatchRegExpConstructor;
22
+ type PartialMatchRegExp = InstanceType<typeof PartialMatchRegExp>;
23
+ export default PartialMatchRegExp;
24
+ export type { PartialMatchRegExp, PartialMatchRegExpConstructor };