re2js 2.8.6 → 2.8.7
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 +94 -104
- package/build/index.cjs +863 -577
- package/build/index.d.cts +284 -219
- package/build/index.d.ts +284 -219
- package/build/index.js +863 -577
- package/build/index.umd.js +863 -577
- package/package.json +30 -24
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching
|
|
2
|
+
|
|
2
3
|
[](https://github.com/le0pard/re2js/actions/workflows/ci.yml)
|
|
3
4
|
|
|
4
5
|
## [Playground](https://re2js.leopard.in.ua/)
|
|
@@ -51,9 +52,9 @@ You can compile a regex pattern using the `compile()` function:
|
|
|
51
52
|
```js
|
|
52
53
|
import { RE2JS } from 're2js'
|
|
53
54
|
|
|
54
|
-
const p = RE2JS.compile('abc')
|
|
55
|
-
console.log(p.pattern())
|
|
56
|
-
console.log(p.flags())
|
|
55
|
+
const p = RE2JS.compile('abc')
|
|
56
|
+
console.log(p.pattern()) // Outputs: 'abc'
|
|
57
|
+
console.log(p.flags()) // Outputs: 0
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
The `compile()` function also supports flags:
|
|
@@ -61,9 +62,9 @@ The `compile()` function also supports flags:
|
|
|
61
62
|
```js
|
|
62
63
|
import { RE2JS } from 're2js'
|
|
63
64
|
|
|
64
|
-
const p = RE2JS.compile('abc', RE2JS.CASE_INSENSITIVE | RE2JS.MULTILINE)
|
|
65
|
-
console.log(p.pattern())
|
|
66
|
-
console.log(p.flags())
|
|
65
|
+
const p = RE2JS.compile('abc', RE2JS.CASE_INSENSITIVE | RE2JS.MULTILINE)
|
|
66
|
+
console.log(p.pattern()) // Outputs: 'abc'
|
|
67
|
+
console.log(p.flags()) // Outputs: 5
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
#### Tagged Template Literals (No Double-Escaping)
|
|
@@ -71,15 +72,15 @@ console.log(p.flags()); // Outputs: 5
|
|
|
71
72
|
To avoid the tedious "double-backslash" escaping problem common with the `RE2JS.compile("\\d+")` syntax, the library provides a handy `re` tagged template literal. This allows you to write patterns exactly as they appear in standard JavaScript regex literals:
|
|
72
73
|
|
|
73
74
|
```js
|
|
74
|
-
import { re, RE2JS } from 're2js'
|
|
75
|
+
import { re, RE2JS } from 're2js'
|
|
75
76
|
|
|
76
77
|
// Instead of RE2JS.compile('\\b\\w+\\b')
|
|
77
|
-
const p1 = re`\b\w+\b
|
|
78
|
-
console.log(p1.pattern())
|
|
78
|
+
const p1 = re`\b\w+\b`
|
|
79
|
+
console.log(p1.pattern()) // Outputs: '\b\w+\b'
|
|
79
80
|
|
|
80
81
|
// You can also pass flags by calling `re` as a function first:
|
|
81
|
-
const p2 = re(RE2JS.CASE_INSENSITIVE | RE2JS.MULTILINE)`^foo\d
|
|
82
|
-
console.log(p2.test('FOO42'))
|
|
82
|
+
const p2 = re(RE2JS.CASE_INSENSITIVE | RE2JS.MULTILINE)`^foo\d+`
|
|
83
|
+
console.log(p2.test('FOO42')) // true
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
#### Supported flags:
|
|
@@ -118,7 +119,7 @@ RE2JS.LOOKBEHINDS
|
|
|
118
119
|
|
|
119
120
|
RE2JS allows you to check if a string matches a given regex pattern using the `matches()` function.
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
_**Performance Note:** The `matches()` method is highly optimized. It performs a strict anchored check and runs directly on the high-speed DFA (Deterministic Finite Automaton) engine without tracking capture groups or instantiating a stateful `Matcher` object._
|
|
122
123
|
|
|
123
124
|
```js
|
|
124
125
|
import { RE2JS } from 're2js'
|
|
@@ -130,10 +131,7 @@ RE2JS.compile('ab+c').matches('abbbc') // true
|
|
|
130
131
|
RE2JS.compile('ab+c').matches('cbbba') // false
|
|
131
132
|
// with flags
|
|
132
133
|
RE2JS.compile('ab+c', RE2JS.CASE_INSENSITIVE).matches('AbBBc') // true
|
|
133
|
-
RE2JS.compile(
|
|
134
|
-
'^ab.*c$',
|
|
135
|
-
RE2JS.DOTALL | RE2JS.MULTILINE | RE2JS.CASE_INSENSITIVE
|
|
136
|
-
).matches('AB\nc') // true
|
|
134
|
+
RE2JS.compile('^ab.*c$', RE2JS.DOTALL | RE2JS.MULTILINE | RE2JS.CASE_INSENSITIVE).matches('AB\nc') // true
|
|
137
135
|
```
|
|
138
136
|
|
|
139
137
|
### High-Performance Boolean Testing
|
|
@@ -145,14 +143,14 @@ If you only need to know **whether** a string matches a pattern (without extract
|
|
|
145
143
|
Tests if the regular expression matches **any part** of the provided input (unanchored). This method mirrors the standard JavaScript `RegExp.prototype.test()` API
|
|
146
144
|
|
|
147
145
|
```js
|
|
148
|
-
import { RE2JS } from 're2js'
|
|
146
|
+
import { RE2JS } from 're2js'
|
|
149
147
|
|
|
150
148
|
// Compile once, reuse often
|
|
151
|
-
const re = RE2JS.compile('error|warning|critical')
|
|
149
|
+
const re = RE2JS.compile('error|warning|critical')
|
|
152
150
|
|
|
153
151
|
// Extremely fast, unanchored DFA search
|
|
154
152
|
if (re.test('The system encountered a critical failure')) {
|
|
155
|
-
console.log('Log needs attention!')
|
|
153
|
+
console.log('Log needs attention!')
|
|
156
154
|
}
|
|
157
155
|
```
|
|
158
156
|
|
|
@@ -160,16 +158,16 @@ if (re.test('The system encountered a critical failure')) {
|
|
|
160
158
|
|
|
161
159
|
Tests if the regular expression matches the entire input string (anchored to both start and end).
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
_Note: `RE2JS.matches()` delegates to this method, so they provide the exact same performance and behavior._
|
|
164
162
|
|
|
165
163
|
```js
|
|
166
|
-
import { RE2JS } from 're2js'
|
|
164
|
+
import { RE2JS } from 're2js'
|
|
167
165
|
|
|
168
|
-
const isHex = RE2JS.compile('[0-9A-Fa-f]+')
|
|
166
|
+
const isHex = RE2JS.compile('[0-9A-Fa-f]+')
|
|
169
167
|
|
|
170
168
|
// Fast, anchored DFA validation
|
|
171
|
-
console.log(isHex.testExact('1A4F'))
|
|
172
|
-
console.log(isHex.testExact('1A4F-xyz'))
|
|
169
|
+
console.log(isHex.testExact('1A4F')) // true
|
|
170
|
+
console.log(isHex.testExact('1A4F-xyz')) // false
|
|
173
171
|
```
|
|
174
172
|
|
|
175
173
|
### Checking Initial Match
|
|
@@ -237,27 +235,26 @@ Instead of dealing with a stateful `Matcher` object, `exec()` performs a single
|
|
|
237
235
|
```js
|
|
238
236
|
import { RE2JS } from 're2js'
|
|
239
237
|
|
|
240
|
-
const re = RE2JS.compile('(?P<first>\\w+) (?:(?P<middle>\\w+) )?(?P<last>\\w+)')
|
|
241
|
-
const result = re.exec('John Doe')
|
|
238
|
+
const re = RE2JS.compile('(?P<first>\\w+) (?:(?P<middle>\\w+) )?(?P<last>\\w+)')
|
|
239
|
+
const result = re.exec('John Doe')
|
|
242
240
|
|
|
243
241
|
if (result !== null) {
|
|
244
|
-
console.log(result[0])
|
|
245
|
-
console.log(result[1])
|
|
246
|
-
console.log(result[2])
|
|
247
|
-
console.log(result[3])
|
|
242
|
+
console.log(result[0]) // "John Doe" (Full match)
|
|
243
|
+
console.log(result[1]) // "John" (Group 1)
|
|
244
|
+
console.log(result[2]) // undefined (Group 2 didn't match)
|
|
245
|
+
console.log(result[3]) // "Doe" (Group 3)
|
|
248
246
|
|
|
249
|
-
console.log(result.index)
|
|
250
|
-
console.log(result.input)
|
|
247
|
+
console.log(result.index) // 0
|
|
248
|
+
console.log(result.input) // "John Doe"
|
|
251
249
|
|
|
252
250
|
// Named groups dictionary
|
|
253
|
-
console.log(result.groups.first)
|
|
254
|
-
console.log(result.groups.middle)
|
|
255
|
-
console.log(result.groups.last)
|
|
251
|
+
console.log(result.groups.first) // "John"
|
|
252
|
+
console.log(result.groups.middle) // undefined
|
|
253
|
+
console.log(result.groups.last) // "Doe"
|
|
256
254
|
}
|
|
257
|
-
|
|
258
255
|
```
|
|
259
256
|
|
|
260
|
-
|
|
257
|
+
_**Performance Note:** If you are running `exec()` inside a `while` loop to manually extract multiple matches globally, it is highly recommended to use `.matchAll()` instead, as it is cleaner, strictly stateless, and avoids infinite loop pitfalls._
|
|
261
258
|
|
|
262
259
|
### Iterating Over Matches (`matchAll`)
|
|
263
260
|
|
|
@@ -275,15 +272,15 @@ const input = 'Dates: 2024-05 and 2025-11.'
|
|
|
275
272
|
|
|
276
273
|
// Native ES6 Iteration
|
|
277
274
|
for (const match of re2.matchAll(input)) {
|
|
278
|
-
console.log(match[0])
|
|
279
|
-
console.log(match.index)
|
|
280
|
-
console.log(match.groups)
|
|
281
|
-
console.log(match.groups.year)
|
|
275
|
+
console.log(match[0]) // "2024-05", then "2025-11"
|
|
276
|
+
console.log(match.index) // 7, then 19
|
|
277
|
+
console.log(match.groups) // { year: '2024', month: '05' } then { year: '2025', month: '11' }
|
|
278
|
+
console.log(match.groups.year) // "2024", then "2025"
|
|
282
279
|
}
|
|
283
280
|
|
|
284
281
|
// Or easily collect all matches into an array
|
|
285
|
-
const allMatches = [...re2.matchAll(input)]
|
|
286
|
-
console.log(allMatches.length)
|
|
282
|
+
const allMatches = [...re2.matchAll(input)]
|
|
283
|
+
console.log(allMatches.length) // 2
|
|
287
284
|
```
|
|
288
285
|
|
|
289
286
|
### Multi-Pattern Matching (RE2Set)
|
|
@@ -301,8 +298,8 @@ const set = new RE2Set()
|
|
|
301
298
|
|
|
302
299
|
// Add patterns to the set.
|
|
303
300
|
// The add() method returns the integer ID of the pattern.
|
|
304
|
-
set.add('error')
|
|
305
|
-
set.add('warning')
|
|
301
|
+
set.add('error') // ID: 0
|
|
302
|
+
set.add('warning') // ID: 1
|
|
306
303
|
set.add('critical') // ID: 2
|
|
307
304
|
|
|
308
305
|
// You must compile the set before matching!
|
|
@@ -331,20 +328,20 @@ import { RE2Set, RE2JS } from 're2js'
|
|
|
331
328
|
|
|
332
329
|
// Anchor the set to match the entire string, make it case-insensitive,
|
|
333
330
|
// and limit the DFA memory usage to ~4MB (default is 8MB).
|
|
334
|
-
const maxMem = 4 * 1024 * 1024
|
|
331
|
+
const maxMem = 4 * 1024 * 1024
|
|
335
332
|
const set = new RE2Set(RE2Set.ANCHOR_BOTH, RE2JS.CASE_INSENSITIVE, maxMem)
|
|
336
333
|
|
|
337
334
|
set.add('foo') // ID: 0
|
|
338
335
|
set.add('bar') // ID: 1
|
|
339
|
-
set.add('.*')
|
|
336
|
+
set.add('.*') // ID: 2
|
|
340
337
|
|
|
341
338
|
set.compile()
|
|
342
339
|
|
|
343
|
-
console.log(set.match('FOO'))
|
|
340
|
+
console.log(set.match('FOO')) // [0, 2] (Matches 'foo' and '.*' because of CASE_INSENSITIVE)
|
|
344
341
|
console.log(set.match('foobar')) // [2] (Only '.*' matches the entire string because of ANCHOR_BOTH)
|
|
345
342
|
```
|
|
346
343
|
|
|
347
|
-
|
|
344
|
+
_**Performance Note:** `RE2Set` heavily utilizes the high-speed DFA engine to process multi-pattern matches simultaneously. However, if your patterns contain boundaries (e.g., `\b`) or trigger a massive state explosion, it will seamlessly and safely fall back to the bounded NFA engine._
|
|
348
345
|
|
|
349
346
|
#### Example: Fast JS Routing with RE2Set
|
|
350
347
|
|
|
@@ -483,9 +480,7 @@ The `group()` method retrieves the content matched by a specific name of capturi
|
|
|
483
480
|
import { RE2JS } from 're2js'
|
|
484
481
|
|
|
485
482
|
// example with `(?P<name>expr)`
|
|
486
|
-
const p = RE2JS.compile(
|
|
487
|
-
'(?P<baz>f(?P<foo>b*a(?P<another>r+)){0,10})(?P<bag>bag)?(?P<nomatch>zzz)?'
|
|
488
|
-
)
|
|
483
|
+
const p = RE2JS.compile('(?P<baz>f(?P<foo>b*a(?P<another>r+)){0,10})(?P<bag>bag)?(?P<nomatch>zzz)?')
|
|
489
484
|
const matchString = p.matcher('fbbarrrrrbag')
|
|
490
485
|
if (matchString.matches()) {
|
|
491
486
|
matchString.group('baz') // 'fbbarrrrr'
|
|
@@ -496,9 +491,7 @@ if (matchString.matches()) {
|
|
|
496
491
|
}
|
|
497
492
|
|
|
498
493
|
// example with `(?<name>expr)`
|
|
499
|
-
const m = RE2JS.compile(
|
|
500
|
-
'(?<baz>f(?<foo>b*a))'
|
|
501
|
-
)
|
|
494
|
+
const m = RE2JS.compile('(?<baz>f(?<foo>b*a))')
|
|
502
495
|
const mString = m.matcher('fbba')
|
|
503
496
|
if (mString.matches()) {
|
|
504
497
|
mString.group('baz') // 'fbba'
|
|
@@ -538,9 +531,7 @@ The `replaceAll()` method replaces all occurrences of a pattern match in a strin
|
|
|
538
531
|
```js
|
|
539
532
|
import { RE2JS } from 're2js'
|
|
540
533
|
|
|
541
|
-
RE2JS.compile('Frog')
|
|
542
|
-
.matcher("What the Frog's Eye Tells the Frog's Brain")
|
|
543
|
-
.replaceAll('Lizard') // "What the Lizard's Eye Tells the Lizard's Brain"
|
|
534
|
+
RE2JS.compile('Frog').matcher("What the Frog's Eye Tells the Frog's Brain").replaceAll('Lizard') // "What the Lizard's Eye Tells the Lizard's Brain"
|
|
544
535
|
RE2JS.compile('(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)')
|
|
545
536
|
.matcher('abcdefghijklmnopqrstuvwxyz123')
|
|
546
537
|
.replaceAll('$10$20') // 'jb0wo0123'
|
|
@@ -549,6 +540,7 @@ RE2JS.compile('(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)')
|
|
|
549
540
|
Note that the replacement string can include references to capturing groups from the pattern
|
|
550
541
|
|
|
551
542
|
Parameters:
|
|
543
|
+
|
|
552
544
|
- `replacement (String | Function)`: The string that replaces the substrings found, or a function invoked to create the new substring. When passing a string, capture groups and special characters have special behavior. For example:
|
|
553
545
|
- `$&` refers to the entire matched substring
|
|
554
546
|
- `$1, $2, ...` refer to the corresponding capture groups in the pattern
|
|
@@ -569,12 +561,8 @@ Examples:
|
|
|
569
561
|
```js
|
|
570
562
|
import { RE2JS } from 're2js'
|
|
571
563
|
|
|
572
|
-
RE2JS.compile('(\\w+) (\\w+)')
|
|
573
|
-
|
|
574
|
-
.replaceAll('$& - $&') // 'Hello World - Hello World'
|
|
575
|
-
RE2JS.compile('(\\w+) (\\w+)')
|
|
576
|
-
.matcher('Hello World')
|
|
577
|
-
.replaceAll('$0 - $0', true) // 'Hello World - Hello World'
|
|
564
|
+
RE2JS.compile('(\\w+) (\\w+)').matcher('Hello World').replaceAll('$& - $&') // 'Hello World - Hello World'
|
|
565
|
+
RE2JS.compile('(\\w+) (\\w+)').matcher('Hello World').replaceAll('$0 - $0', true) // 'Hello World - Hello World'
|
|
578
566
|
```
|
|
579
567
|
|
|
580
568
|
#### Replacing the First Occurrence
|
|
@@ -584,9 +572,7 @@ The `replaceFirst()` method replaces the first occurrence of a pattern match in
|
|
|
584
572
|
```js
|
|
585
573
|
import { RE2JS } from 're2js'
|
|
586
574
|
|
|
587
|
-
RE2JS.compile('Frog')
|
|
588
|
-
.matcher("What the Frog's Eye Tells the Frog's Brain")
|
|
589
|
-
.replaceFirst('Lizard') // "What the Lizard's Eye Tells the Frog's Brain"
|
|
575
|
+
RE2JS.compile('Frog').matcher("What the Frog's Eye Tells the Frog's Brain").replaceFirst('Lizard') // "What the Lizard's Eye Tells the Frog's Brain"
|
|
590
576
|
RE2JS.compile('(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)')
|
|
591
577
|
.matcher('abcdefghijklmnopqrstuvwxyz123')
|
|
592
578
|
.replaceFirst('$10$20') // 'jb0nopqrstuvwxyz123'
|
|
@@ -610,23 +596,21 @@ The replacer function is invoked for each match, and its return value is used as
|
|
|
610
596
|
import { RE2JS } from 're2js'
|
|
611
597
|
|
|
612
598
|
// Example 1: Dynamic replacements
|
|
613
|
-
const re1 = RE2JS.compile('\\d+')
|
|
614
|
-
const m1 = re1.matcher('Numbers: 1, 2, 3')
|
|
599
|
+
const re1 = RE2JS.compile('\\d+')
|
|
600
|
+
const m1 = re1.matcher('Numbers: 1, 2, 3')
|
|
615
601
|
|
|
616
|
-
m1.replaceAll((match) => String(Number(match) * 10))
|
|
602
|
+
m1.replaceAll((match) => String(Number(match) * 10))
|
|
617
603
|
// 'Numbers: 10, 20, 30'
|
|
618
604
|
|
|
619
|
-
|
|
620
605
|
// Example 2: Using named capture groups and function signature
|
|
621
|
-
const re2 = RE2JS.compile('(?P<first>\\w+) (?:(?P<middle>\\w+) )?(?P<last>\\w+)')
|
|
622
|
-
const m2 = re2.matcher('Hello World')
|
|
606
|
+
const re2 = RE2JS.compile('(?P<first>\\w+) (?:(?P<middle>\\w+) )?(?P<last>\\w+)')
|
|
607
|
+
const m2 = re2.matcher('Hello World')
|
|
623
608
|
|
|
624
609
|
m2.replaceFirst((match, p1, p2, p3, offset, string, groups) => {
|
|
625
610
|
// 'middle' didn't match, so p2 and groups.middle will be undefined
|
|
626
|
-
return `${groups.last}, ${groups.first}
|
|
627
|
-
})
|
|
611
|
+
return `${groups.last}, ${groups.first}`
|
|
612
|
+
})
|
|
628
613
|
// 'World, Hello'
|
|
629
|
-
|
|
630
614
|
```
|
|
631
615
|
|
|
632
616
|
### Safe Replacements
|
|
@@ -671,9 +655,9 @@ The program size represents a very approximate measure of a regexp's "cost". Lar
|
|
|
671
655
|
```js
|
|
672
656
|
import { RE2JS } from 're2js'
|
|
673
657
|
|
|
674
|
-
console.log(RE2JS.compile('^').programSize())
|
|
675
|
-
console.log(RE2JS.compile('a+b').programSize())
|
|
676
|
-
console.log(RE2JS.compile('(a+b?)').programSize())
|
|
658
|
+
console.log(RE2JS.compile('^').programSize()) // Outputs: 3
|
|
659
|
+
console.log(RE2JS.compile('a+b').programSize()) // Outputs: 5
|
|
660
|
+
console.log(RE2JS.compile('(a+b?)').programSize()) // Outputs: 8
|
|
677
661
|
```
|
|
678
662
|
|
|
679
663
|
### Translating Regular Expressions
|
|
@@ -710,20 +694,20 @@ Originally, the C++ implementation of the RE2 engine included both NFA (Nondeter
|
|
|
710
694
|
|
|
711
695
|
`re2js` achieves full architectural parity with the highly optimized Go `regexp` package and incorporates advanced performance features from the original C++ engine. To maximize execution speed on everyday queries without ever sacrificing memory safety, `re2js` intelligently and dynamically routes execution through a highly advanced multi-tiered architecture:
|
|
712
696
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
697
|
+
- **The Prefilter Engine:** Analyzes the Abstract Syntax Tree (AST) before execution to extract mandatory string literals (e.g., extracting `"error"` and `"critical"` from `/error.*critical/`). It uses blistering-fast native JavaScript `indexOf` to instantly reject mismatches, completely bypassing the regex state-machines.
|
|
698
|
+
- **Aggressive AST Simplification:** Trims impossible match branches and collapses redundant logic prior to compilation, mathematically pruning dead execution paths to dramatically reduce the size of the generated state machine.
|
|
699
|
+
- **Multi-Pattern Sets (`RE2Set`):** Combines hundreds or thousands of regular expressions into a single combined DFA, allowing you to search a string for all patterns simultaneously in strict $O(N)$ linear time.
|
|
700
|
+
- **OnePass DFA:** Provides high-speed capture group extraction for mathematically 1-unambiguous patterns, bypassing thread queues entirely.
|
|
701
|
+
- **Lazy Powerset DFA:** Executes high-speed boolean matches (e.g., `.test()`) by fusing active states dynamically on the fly.
|
|
702
|
+
- **BitState Backtracker:** Avoids heavy object array allocations by using bitwise operations to extract captures on short-to-medium length strings.
|
|
703
|
+
- **Pike VM (NFA):** Acts as the robust, bounded-memory fallback engine for complex, ambiguous patterns that exceed fast-path limits.
|
|
720
704
|
|
|
721
705
|
Thanks to these dynamic fast-paths, `re2js` delivers performance comparable to native engines for simple queries, while remaining completely immune to catastrophic backtracking and stack overflow crashes.
|
|
722
706
|
|
|
723
707
|
Should you require maximum absolute performance on the server side when using RE2, it would be beneficial to consider the following packages for JS:
|
|
724
708
|
|
|
725
|
-
|
|
726
|
-
|
|
709
|
+
- [Node-RE2](https://github.com/uhop/node-re2/): A powerful RE2 C++ binding for Node.js
|
|
710
|
+
- [RE2-WASM](https://github.com/google/re2-wasm/): This package is a WASM wrapper for RE2. Please note, as of now, it does not work in browsers
|
|
727
711
|
|
|
728
712
|
### RE2JS vs RE2-Node (C++ Bindings)
|
|
729
713
|
|
|
@@ -732,7 +716,7 @@ Because RE2JS's Lazy DFA, Prefilter, and OnePass engines operate efficiently wit
|
|
|
732
716
|
Here is a [benchmark running 30,000 items](src/__tests__/re2.bench.js) through both engines using their respective `.test()` fast-paths (averages of multiple runs):
|
|
733
717
|
|
|
734
718
|
| Benchmark Scenario | Pattern Example | RE2JS (Pure JS) | RE2-Node (C++) | Result |
|
|
735
|
-
|
|
719
|
+
| :------------------------ | :------------------------- | :-------------- | :------------- | :-------------------------- |
|
|
736
720
|
| **ReDoS Attempt** | `/(a+)+!/` | **2.16 ms** | 15.94 ms | `re2js` is **7.38x** faster |
|
|
737
721
|
| **Simple Literal** | `/damage/` | **2.58 ms** | 12.39 ms | `re2js` is **4.80x** faster |
|
|
738
722
|
| **Deep State Machine** | `/([0-9]+(/[0-9]+)+)/` | **11.20 ms** | 15.76 ms | `re2js` is **1.41x** faster |
|
|
@@ -744,10 +728,10 @@ Here is a [benchmark running 30,000 items](src/__tests__/re2.bench.js) through b
|
|
|
744
728
|
| **Word Boundaries (NFA)** | `/\b(Flying\|First...)\b/` | 107.12 ms | **15.41 ms** | `re2-node` is 6.95x faster |
|
|
745
729
|
|
|
746
730
|
**Takeaways:**
|
|
747
|
-
* **Pure JS Strengths:** For complex state tracking (nested groups, wildcards) and literal string scanning, `re2js` actually beats the native C++ bindings. V8's Turbofan JIT compiler is able to heavily optimize the Pure JS DFA loop, bypassing the C++ boundary entirely.
|
|
748
|
-
* **C++ Strengths:** For character class evaluations (Case Insensitivity, Bounded Repetitions), `re2-node` has a slight edge thanks to highly optimized, hardware-level memory tables.
|
|
749
|
-
* **The NFA Fallback:** Pure DFA engines mathematically cannot track look-behind context like Word Boundaries (`\b`). When RE2JS encounters these, it safely bails out to its NFA engine. As shown in the benchmarks, the pure JS NFA is significantly slower than the C++ NFA. **For maximum performance in RE2JS, avoid `\b` when doing bulk boolean `.test()` matching.**
|
|
750
731
|
|
|
732
|
+
- **Pure JS Strengths:** For complex state tracking (nested groups, wildcards) and literal string scanning, `re2js` actually beats the native C++ bindings. V8's Turbofan JIT compiler is able to heavily optimize the Pure JS DFA loop, bypassing the C++ boundary entirely.
|
|
733
|
+
- **C++ Strengths:** For character class evaluations (Case Insensitivity, Bounded Repetitions), `re2-node` has a slight edge thanks to highly optimized, hardware-level memory tables.
|
|
734
|
+
- **The NFA Fallback:** Pure DFA engines mathematically cannot track look-behind context like Word Boundaries (`\b`). When RE2JS encounters these, it safely bails out to its NFA engine. As shown in the benchmarks, the pure JS NFA is significantly slower than the C++ NFA. **For maximum performance in RE2JS, avoid `\b` when doing bulk boolean `.test()` matching.**
|
|
751
735
|
|
|
752
736
|
### RE2JS vs JavaScript's native RegExp
|
|
753
737
|
|
|
@@ -759,7 +743,7 @@ const string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
|
|
|
759
743
|
|
|
760
744
|
// Running 30,000 iterations
|
|
761
745
|
RE2JS.compile(regex).test(string) // Total time: ~9.87 ms
|
|
762
|
-
new RegExp(regex).test(string)
|
|
746
|
+
new RegExp(regex).test(string) // Total time: ~11.43 ms
|
|
763
747
|
```
|
|
764
748
|
|
|
765
749
|
For safe, simple patterns, the RE2JS DFA fast-path is heavily optimized and performs at parity with—or even slightly faster than—V8's native RegExp engine.
|
|
@@ -771,7 +755,7 @@ const string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
|
|
|
771
755
|
// Running 30,000 iterations
|
|
772
756
|
RE2JS.compile(regex).test(string) // Total time: ~454.17 ms
|
|
773
757
|
// Running EXACTLY 1 iteration
|
|
774
|
-
new RegExp(regex).test(string)
|
|
758
|
+
new RegExp(regex).test(string) // Total time: ~105802.02 ms (over 105 seconds)
|
|
775
759
|
```
|
|
776
760
|
|
|
777
761
|
In the second example, a ReDoS scenario is depicted. The regular expression `([a-z]+)+$` is a potentially problematic one because it contains a nested quantifier. In standard NFA engines (like JavaScript's native `RegExp`), nested quantifiers can cause catastrophic backtracking. If a malicious user inputs a carefully crafted string, it results in exponentially high processing times, leading to a Denial of Service (DoS) attack.
|
|
@@ -787,25 +771,25 @@ However, `re2js` implements a breakthrough algorithmic approach ([developed by r
|
|
|
787
771
|
You can enable it by passing the `RE2JS.LOOKBEHINDS` flag during compilation:
|
|
788
772
|
|
|
789
773
|
```js
|
|
790
|
-
import { RE2JS } from 're2js'
|
|
774
|
+
import { RE2JS } from 're2js'
|
|
791
775
|
|
|
792
776
|
// Positive Lookbehind: Match 'bar' only if preceded by 'foo'
|
|
793
|
-
const positive = RE2JS.compile('(?<=foo)bar', RE2JS.LOOKBEHINDS)
|
|
794
|
-
positive.test('foobar')
|
|
795
|
-
positive.test('bazbar')
|
|
777
|
+
const positive = RE2JS.compile('(?<=foo)bar', RE2JS.LOOKBEHINDS)
|
|
778
|
+
positive.test('foobar') // true
|
|
779
|
+
positive.test('bazbar') // false
|
|
796
780
|
|
|
797
781
|
// Negative Lookbehind: Match 'bar' only if NOT preceded by 'foo'
|
|
798
|
-
const negative = RE2JS.compile('(?<!foo)bar', RE2JS.LOOKBEHINDS)
|
|
799
|
-
negative.test('bazbar')
|
|
800
|
-
negative.test('foobar')
|
|
782
|
+
const negative = RE2JS.compile('(?<!foo)bar', RE2JS.LOOKBEHINDS)
|
|
783
|
+
negative.test('bazbar') // true
|
|
784
|
+
negative.test('foobar') // false
|
|
801
785
|
```
|
|
802
786
|
|
|
803
787
|
### Important Limitations and Warnings
|
|
804
788
|
|
|
805
789
|
1. **Performance Overhead:** If a regex contains a lookbehind, the engine is forced to safely bypass the ultra-fast Lazy DFA and OnePass engines. It evaluates the lookbehinds using parallel automata running on the NFA (Pike VM). While execution remains mathematically safe and linear $O(n)$, the NFA engine is generally slower than the DFA fast-paths. Use lookbehinds only when necessary.
|
|
806
|
-
2. **Prefix Acceleration is Disabled:**
|
|
807
|
-
3. **Captureless Guarantee:** To prevent state-explosion vulnerabilities and maintain strict safety invariants, lookbehinds are strictly evaluated as
|
|
808
|
-
|
|
790
|
+
2. **Prefix Acceleration is Disabled:** The lookbehind automata must observe every character of the input, so high-speed string prefix skipping (e.g., using `indexOf` to jump to a starting literal) is disabled when lookbehinds are present.
|
|
791
|
+
3. **Captureless Guarantee:** To prevent state-explosion vulnerabilities and maintain strict safety invariants, lookbehinds are strictly evaluated as _captureless_. If you attempt to include a capturing group inside a lookbehind (e.g., `(?<=(foo))bar`), the engine will proactively throw a `SyntaxError` at compile time. Use non-capturing groups `(?:...)` instead.
|
|
792
|
+
4. **Repeated Matching:** The lookbehind automata run from the beginning of the input, because a lookbehind may depend on text arbitrarily far behind the match (e.g. `(?<=x.*)b`). A `Matcher` therefore carries their state from one match to the next: `find()` loops, `matchAll()`, `split()` and `replaceAll()` scan the input once and stay linear in its length. Only a `find(start)` that jumps _backwards_ (to a position before the previous search started) re-evaluates the lookbehinds from the beginning of the input.
|
|
809
793
|
|
|
810
794
|
## Development
|
|
811
795
|
|
|
@@ -818,4 +802,10 @@ yarn node ./tools/scripts/genUnicodeTable.js > src/UnicodeTables.js
|
|
|
818
802
|
|
|
819
803
|
To run `make_perl_groups.pl`, you need to have Perl installed (the required version is specified inside `.tool-versions`).
|
|
820
804
|
|
|
805
|
+
The conformance tests in `src/__tests__/Exec.test.js` run the RE2 and AT&T POSIX test data through every matching engine, on both UTF-16 and UTF-8 input. By default they check a sample of the 5.8 million-line exhaustive RE2 data; to check all of it (this takes several minutes):
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
RE2JS_EXHAUSTIVE=1 yarn vitest run --project=node src/__tests__/Exec.test.js
|
|
809
|
+
```
|
|
810
|
+
|
|
821
811
|
[Playground website](https://re2js.leopard.in.ua/) maintained in `www` branch
|