regex-partial-match 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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, [`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.
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
 
@@ -69,19 +69,17 @@ state("2024-06-15"); // 'complete' - accept, enable submit
69
69
  ```
70
70
 
71
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.
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
73
 
74
74
  Upgrading from 1.x, where this was `isComplete()`? See the [migration guide](./codemods/transforms/v1-v2/README.md).
75
75
 
76
- [^1]:
77
- Mimicking JDK's [`Matcher.hitEnd()`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--)
76
+ [^1]: Mimicking JDK's [`Matcher.hitEnd()`](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#hitEnd--)
78
77
 
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.
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.
81
79
 
82
80
  ### A note on Tree-Shaking
83
81
 
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:
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:
85
83
 
86
84
  ```javascript
87
85
  import PartialMatchRegExp from "regex-partial-match/partialMatchRegExp";
@@ -91,14 +89,24 @@ import features from "regex-partial-match/features";
91
89
 
92
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).
93
91
 
92
+ To tell an instance from a plain `RegExp` without importing a class, use [`isPartialMatchRegExp()`](#ispartialmatchregexp). It returns `true` for an instance of any `PartialMatchRegExp` class and `false` for anything else, including a plain `RegExp`:
93
+
94
+ ```javascript
95
+ import PartialMatchRegExp from "regex-partial-match/core";
96
+ import { isPartialMatchRegExp } from "regex-partial-match/features";
97
+
98
+ isPartialMatchRegExp(new PartialMatchRegExp(/a/)); // true
99
+ isPartialMatchRegExp(/a/); // false
100
+ ```
101
+
94
102
  ### The lean entry: `regex-partial-match/core`
95
103
 
96
104
  `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`:
97
105
 
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 |
106
+ | Module | Import | Supplies |
107
+ | ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
108
+ | `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 |
109
+ | `backreferences` | `regex-partial-match/modules/backreferences` | matching a backreference against the text its group captured |
102
110
 
103
111
  Both are also named exports of `regex-partial-match/modules`.
104
112
 
@@ -113,7 +121,7 @@ const WithBoth = withModules(carets, backreferences);
113
121
  new WithBoth(/(a|b)\1/).test("a"); // true
114
122
  ```
115
123
 
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.
124
+ `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.
117
125
 
118
126
  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.
119
127
 
@@ -121,11 +129,11 @@ A module's type is `Module`, exported from both `regex-partial-match/core` and `
121
129
 
122
130
  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.
123
131
 
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 |
132
+ | Error when constructed | Bind |
133
+ | -------------------------------------------------------- | ---------------- |
134
+ | `TypeError: Needs the carets module` | `carets` |
135
+ | `TypeError: Needs the backreferences module` | `backreferences` |
136
+ | `TypeError: Needs the carets and backreferences modules` | both |
129
137
 
130
138
  The error names every module the pattern needs, and none that is already bound.
131
139
 
@@ -180,7 +188,7 @@ The library is compiled to **ES2015** (ECMAScript 6). Certain regular expression
180
188
  - [**`v` (unicodeSets) flag**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) - ES2024+
181
189
  - [**Modifiers**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Modifier) (`(?ims:...)`, `(?-ims:...)`, `(?i-ms:...)`) - ES2025+
182
190
 
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.
191
+ 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.
184
192
 
185
193
  ## ⚠️ Caveats
186
194
 
@@ -323,7 +331,7 @@ Available as a named export of the default entry point, `import { hitEnd } from
323
331
  **Returns:**
324
332
 
325
333
  - `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.
334
+ - `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 three cases in [What it cannot see](#what-it-cannot-see) below, where a read of the end leaves no marker, or a split surrogate pair can still change the match, and `false` is reported despite it.
327
335
 
328
336
  ```javascript
329
337
  import PartialMatchRegExp, { hitEnd } from "regex-partial-match";
@@ -340,14 +348,15 @@ hitEnd(greedy, greedy.exec("hello world")); // true — \w+ read the end lookin
340
348
  ```
341
349
 
342
350
  > [!NOTE]
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.
351
+ > 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 three limits in [What it cannot see](#what-it-cannot-see), it is never wrong in the other direction.
344
352
 
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.
353
+ 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.
346
354
 
347
355
  #### What it cannot see
348
356
 
349
357
  - **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
358
  - **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.
359
+ - **A high surrogate ending the input under `u` or `v`.** Only whole astral characters are supported (see [Surrogate Pair Matching](./docs/caveats.md#surrogate-pair-matching)), so a match whose last code unit is a lone high surrogate reports `false`, although a following low surrogate would pair with it and change the match's text: `/./u` on `"\ud83d"` matches `"\ud83d"`, but `"😄"` matches `"😄"`. [^3]
351
360
 
352
361
  ### `features()`
353
362
 
@@ -355,11 +364,11 @@ See [How It Works](./docs/how-it-works.md#why-the-question-cant-be-answered-from
355
364
  features(partial: PartialMatchRegExp): ReadonlySet<RegexFeature>
356
365
  ```
357
366
 
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.
367
+ Building the partial-match regex requires walking the entire source pattern, usually once. [^4] 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.
359
368
 
360
369
  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).
361
370
 
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:
371
+ This is useful for consumers building on top of `PartialMatchRegExp` who need to reason about which constructs a _specific_ pattern uses, without writing their own regex parser to find out. Two concrete cases:
363
372
 
364
373
  - **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
374
  - **Restricting which constructs a product surface allows.** e.g. a system that only wants to accept "simple" patterns (no lookaround, no backreferences) from untrusted input can check `features()` against an allow-list and reject the rest, without needing to hand-roll that check against the raw pattern source.
@@ -376,38 +385,38 @@ features(partial).has("backreference"); // true
376
385
 
377
386
  `RegexFeature` is a string union, exported from `regex-partial-match` and `regex-partial-match/features`:
378
387
 
379
- | Feature | Matches | Notes |
380
- | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
381
- | `patternCharacter` | An ordinary literal character | |
382
- | `startAnchor` | Top-level `^` | |
383
- | `endAnchor` | Top-level `$` | |
384
- | `wordBoundary` | Top-level `\b` | |
385
- | `nonWordBoundary` | Top-level `\B` | |
386
- | `lookahead` | `(?=...)` | |
387
- | `negativeLookahead` | `(?!...)` | |
388
- | `lookbehind` | `(?<=...)` | |
389
- | `negativeLookbehind` | `(?<!...)` | |
390
- | `backreference` | `\1` | |
391
- | `namedBackreference` | `\k<name>` | |
392
- | `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
393
- | `capturingGroup` | `(...)`, including named groups | |
394
- | `lookaroundCapture` | A capturing group inside any lookaround | Nesting, not a construct — always accompanied by `capturingGroup` |
395
- | `nonCapturingGroup` | `(?:...)` | |
396
- | `modifierGroup` | `(?ims:...)` | |
397
- | `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
398
- | `characterClass` | `[...]` | |
399
- | `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
400
- | `classIntersection` | `&&` inside a character class | `v` flag only |
401
- | `classSubtraction` | `--` inside a character class | `v` flag only |
402
- | `disjunction` | `\|` | |
403
- | `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
404
- | `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only — otherwise tagged `otherEscape` |
405
- | `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
406
- | `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
407
- | `controlLetterEscape` | `\cX` | |
408
- | `hexEscapeSequence` | `\xXX` | |
409
- | `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
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) |
388
+ | Feature | Matches | Notes |
389
+ | -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
390
+ | `patternCharacter` | An ordinary literal character | |
391
+ | `startAnchor` | Top-level `^` | |
392
+ | `endAnchor` | Top-level `$` | |
393
+ | `wordBoundary` | Top-level `\b` | |
394
+ | `nonWordBoundary` | Top-level `\B` | |
395
+ | `lookahead` | `(?=...)` | |
396
+ | `negativeLookahead` | `(?!...)` | |
397
+ | `lookbehind` | `(?<=...)` | |
398
+ | `negativeLookbehind` | `(?<!...)` | |
399
+ | `backreference` | `\1` | |
400
+ | `namedBackreference` | `\k<name>` | |
401
+ | `namedGroup` | `(?<name>...)` | Always accompanied by `capturingGroup` — see below |
402
+ | `capturingGroup` | `(...)`, including named groups | |
403
+ | `lookaroundCapture` | A capturing group inside any lookaround | Nesting, not a construct — always accompanied by `capturingGroup` |
404
+ | `nonCapturingGroup` | `(?:...)` | |
405
+ | `modifierGroup` | `(?ims:...)` | |
406
+ | `modifierGroupWithRemoval` | `(?ims-ims:...)` | Mutually exclusive with `modifierGroup` |
407
+ | `characterClass` | `[...]` | |
408
+ | `nestedCharacterClass` | `[...[...]...]` | `v` flag only |
409
+ | `classIntersection` | `&&` inside a character class | `v` flag only |
410
+ | `classSubtraction` | `--` inside a character class | `v` flag only |
411
+ | `disjunction` | `\|` | |
412
+ | `quantifier` | `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` | |
413
+ | `unicodePropertyEscape` | `\p{...}`, `\P{...}` | `u`/`v` flag only — otherwise tagged `otherEscape` |
414
+ | `characterClassEscape` | `\d`, `\D`, `\w`, `\W`, `\s`, `\S` | |
415
+ | `controlEscape` | `\f`, `\n`, `\r`, `\t`, `\v` | |
416
+ | `controlLetterEscape` | `\cX` | |
417
+ | `hexEscapeSequence` | `\xXX` | |
418
+ | `unicodeEscapeSequence` | `\uXXXX`, `\u{...}` | |
419
+ | `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) |
411
420
 
412
421
  Three things worth knowing about how these tags line up with the grammar:
413
422
 
@@ -415,6 +424,23 @@ Three things worth knowing about how these tags line up with the grammar:
415
424
  - **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.
416
425
  - **`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.
417
426
 
427
+ [^3]: A scanner fed from a source that can split a surrogate pair across chunks, rather than a [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) in streaming mode (`stream: true`, or `TextDecoderStream`), should defer any match ending on a code unit in `0xD800`–`0xDBFF` at the end of the input.
428
+
429
+ [^4]: 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.
430
+
431
+ ### `isPartialMatchRegExp()`
432
+
433
+ ```typescript
434
+ isPartialMatchRegExp(value: unknown): value is PartialMatchRegExp
435
+ ```
436
+
437
+ Returns `true` for an instance of any `PartialMatchRegExp` class, from `regex-partial-match/core`, from `withModules(...)`, or from the default entry point, and `false` for a plain `RegExp` or any other value. It doesn't import a class or module, so it can be used to accept either a `RegExp` or a ready-built instance without pulling the default class into a bundle.
438
+
439
+ Available as a named export of `regex-partial-match/features`, or of the default entry point, `import { isPartialMatchRegExp } from 'regex-partial-match'`.
440
+
441
+ > [!CAUTION]
442
+ > It recognises instances from the same installed copy of the library only: an instance from a second copy, such as a duplicate install in a bundle, returns `false`.
443
+
418
444
  ## 📜 License
419
445
 
420
446
  ISC License - see [LICENSE](./LICENSE) file for details.
@@ -1,6 +1,7 @@
1
1
  import type PartialMatchRegExp from "../partialMatchRegExp/index.ts";
2
2
  import { type RegexFeature } from "../partialMatchRegExp/regexFeatures.ts";
3
3
  export type { RegexFeature };
4
+ export { default as isPartialMatchRegExp } from "../partialMatchRegExp/isPartialMatchRegExp.ts";
4
5
  /**
5
6
  * The syntactic constructs `partial`'s original pattern uses.
6
7
  *
@@ -1,5 +1,6 @@
1
1
  import { compiledOf } from "../partialMatchRegExp/partialMatchInternals.js";
2
2
  import { featureSet } from "../partialMatchRegExp/regexFeatures.js";
3
+ export { default as isPartialMatchRegExp } from "../partialMatchRegExp/isPartialMatchRegExp.js";
3
4
  /**
4
5
  * The syntactic constructs `partial`'s original pattern uses.
5
6
  *
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./partialMatchRegExp/index.ts";
2
2
  export { default } from "./partialMatchRegExp/index.ts";
3
3
  export { default as hitEnd } from "./hitEnd/index.ts";
4
- export { default as features } from "./features/index.ts";
4
+ export { default as features, isPartialMatchRegExp } from "./features/index.ts";
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./partialMatchRegExp/index.js";
2
2
  export { default } from "./partialMatchRegExp/index.js";
3
3
  export { default as hitEnd } from "./hitEnd/index.js";
4
- export { default as features } from "./features/index.js";
4
+ export { default as features, isPartialMatchRegExp } from "./features/index.js";
@@ -1,11 +1,12 @@
1
1
  import compileWith from "./compilePartial/compileWith.js";
2
- import { compiledPartial } from "./partialMatchInternals.js";
2
+ import { compiledPartial, constructedPartials } from "./partialMatchInternals.js";
3
3
  import execFrom from "./execFrom.js";
4
4
  export default function createPartialMatchRegExp(hooks) {
5
5
  return class PartialMatchRegExp extends RegExp {
6
6
  constructor(pattern, flags) {
7
7
  super(pattern, flags);
8
8
  this[compiledPartial] = compileWith(this, hooks);
9
+ constructedPartials.add(this);
9
10
  }
10
11
  exec(input) {
11
12
  const compiled = this[compiledPartial];
@@ -0,0 +1,19 @@
1
+ import type { PartialMatchRegExp } from "./index.ts";
2
+ /**
3
+ * Whether `value` is an instance of a `PartialMatchRegExp` class, from any
4
+ * entry point or `withModules`, without importing any of those classes.
5
+ *
6
+ * @param value - Any value
7
+ * @returns `true` for a `PartialMatchRegExp` instance; `false` for a plain
8
+ * `RegExp` or any other value
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import PartialMatchRegExp from "regex-partial-match/core";
13
+ * import { isPartialMatchRegExp } from "regex-partial-match/features";
14
+ *
15
+ * isPartialMatchRegExp(new PartialMatchRegExp(/a/)); // true
16
+ * isPartialMatchRegExp(/a/); // false
17
+ * ```
18
+ */
19
+ export default function isPartialMatchRegExp(value: unknown): value is PartialMatchRegExp;
@@ -0,0 +1,23 @@
1
+ import { constructedPartials } from "./partialMatchInternals.js";
2
+ /**
3
+ * Whether `value` is an instance of a `PartialMatchRegExp` class, from any
4
+ * entry point or `withModules`, without importing any of those classes.
5
+ *
6
+ * @param value - Any value
7
+ * @returns `true` for a `PartialMatchRegExp` instance; `false` for a plain
8
+ * `RegExp` or any other value
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import PartialMatchRegExp from "regex-partial-match/core";
13
+ * import { isPartialMatchRegExp } from "regex-partial-match/features";
14
+ *
15
+ * isPartialMatchRegExp(new PartialMatchRegExp(/a/)); // true
16
+ * isPartialMatchRegExp(/a/); // false
17
+ * ```
18
+ */
19
+ export default function isPartialMatchRegExp(value) {
20
+ return (typeof value === "object" &&
21
+ value !== null &&
22
+ constructedPartials.has(value));
23
+ }
@@ -1,3 +1,4 @@
1
1
  import type { CompiledPartial } from "./compilePartial/compiled.ts";
2
2
  export declare const compiledPartial: unique symbol;
3
+ export declare const constructedPartials: WeakSet<object>;
3
4
  export declare function compiledOf(partial: RegExp): CompiledPartial;
@@ -1,4 +1,5 @@
1
1
  export const compiledPartial = Symbol("compiledPartial");
2
+ export const constructedPartials = new WeakSet();
2
3
  export function compiledOf(partial) {
3
4
  return partial[compiledPartial];
4
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regex-partial-match",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A regular expression transform for partial matching",
5
5
  "keywords": [
6
6
  "regexp",