Skip to content

Commit e61dcdf

Browse files
committed
Remove the rregex sugar tag
1 parent 363a004 commit e61dcdf

File tree

5 files changed

+53
-64
lines changed

5 files changed

+53
-64
lines changed

README.md

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# regex-recursion [![npm](https://img.shields.io/npm/v/regex-recursion)](https://www.npmjs.com/package/regex-recursion)
1+
# regex-recursion
2+
3+
[![build status](https://github.com/slevithan/regex-recursion/workflows/CI/badge.svg)](https://github.com/slevithan/regex-recursion/actions)
4+
[![npm](https://img.shields.io/npm/v/regex-recursion)](https://www.npmjs.com/package/regex-recursion)
5+
[![bundle size](https://deno.bundlejs.com/badge?q=regex-recursion&treeshake=[*])](https://bundlejs.com/?q=regex-recursion&treeshake=[*])
26

37
This is a plugin for the [`regex`](https://github.com/slevithan/regex) library that adds support for recursive matching up to a specified max depth *N*, where *N* must be between 2 and 100. Generated regexes are native `RegExp` instances, and support all JavaScript regular expression features.
48

@@ -13,19 +17,26 @@ Recursive matching supports named captures/backreferences, and makes them indepe
1317
## Install and use
1418

1519
```sh
16-
npm install regex-recursion
20+
npm install regex regex-recursion
1721
```
1822

1923
```js
20-
import {rregex} from 'regex-recursion';
24+
import {regex} from 'regex';
25+
import {recursion} from 'regex-recursion';
26+
27+
const re = regex({plugins: [recursion]})``;
2128
```
2229

23-
In browsers:
30+
In browsers (using a global name):
2431

2532
```html
33+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/regex.min.js"></script>
2634
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/regex-recursion.min.js"></script>
2735
<script>
28-
const {rregex} = Regex.ext;
36+
const {regex} = Regex;
37+
const {recursion} = Regex.plugins;
38+
39+
const re = regex({plugins: [recursion]})``;
2940
</script>
3041
```
3142

@@ -37,14 +48,15 @@ In browsers:
3748

3849
```js
3950
// Matches sequences of up to 50 'a' chars followed by the same number of 'b'
40-
rregex`a(?R=50)?b`.exec('test aaaaaabbb')[0];
51+
const re = regex({plugins: [recursion]})`a(?R=50)?b`;
52+
re.exec('test aaaaaabbb')[0];
4153
// → 'aaabbb'
4254
```
4355

4456
#### As the entire string
4557

4658
```js
47-
const re = rregex`^
59+
const re = regex({plugins: [recursion]})`^
4860
(?<balanced>
4961
a
5062
# Recursively match just the specified group
@@ -60,7 +72,7 @@ re.test('aaabb'); // → false
6072

6173
```js
6274
// Matches all balanced parentheses up to depth 50
63-
const parens = rregex('g')`\(
75+
const parens = regex({flags: 'g', plugins: [recursion]})`\(
6476
( [^\(\)] | (?R=50) )*
6577
\)`;
6678

@@ -76,35 +88,39 @@ const parens = rregex('g')`\(
7688
Here's an alternative that matches the same strings, but adds a nested quantifier. It then uses an atomic group to prevent this nested quantifier from creating the potential for runaway backtracking:
7789

7890
```js
79-
const parens = rregex('g')`\(
91+
const parens = regex({flags: 'g', plugins: [recursion]})`\(
8092
( (?> [^\(\)]+ ) | (?R=50) )*
8193
\)`;
8294
```
8395

8496
This matches sequences of non-parens in one step with the nested `+` quantifier, and avoids backtracking into these sequences by wrapping it with an atomic group `(?>…)`. Given that what the nested quantifier `+` matches overlaps with what the outer group can match with its `*` quantifier, the atomic group is important here. It avoids runaway backtracking when matching long strings with unbalanced parens.
8597

86-
Atomic groups are provided by the base `regex` package.
98+
Atomic groups are provided by the base `regex` library.
8799

88100
### Match palindromes
89101

90102
#### Match palindroms anywhere within a string
91103

92104
```js
93-
const palindromes = rregex('gi')`(?<char>\w) ((?R=15)|\w?) \k<char>`;
105+
const palindromes = regex({flags: 'gi', plugins: [recursion]})`
106+
(?<char> \w )
107+
# Recurse, or match a lone unbalanced char in the middle
108+
( (?R=15) | \w? )
109+
\k<char>
110+
`;
94111

95112
'Racecar, ABBA, and redivided'.match(palindromes);
96113
// → ['Racecar', 'ABBA', 'edivide']
97114
```
98115

99-
In this example, the max length of matched palindromes is 31. That's because it sets the max recursion depth to 15 with `(?R=15)`. So, depth 15 × 2 chars (left + right) for each depth level + 1 optional unbalanced char in the center = 31. To match longer palindromes, the max recursion depth can be increased to a max of 100, which would enable matching palindromes up to 201 characters long.
116+
In the example above, the max length of matched palindromes is 31. That's because it sets the max recursion depth to 15 with `(?R=15)`. So, depth 15 × 2 chars (left + right) for each depth level + 1 optional unbalanced char in the middle = 31. To match longer palindromes, the max recursion depth can be increased to a max of 100, which would enable matching palindromes up to 201 characters long.
100117

101118
#### Match palindromes as complete words
102119

103120
```js
104-
const palindromeWords = rregex('gi')`\b
121+
const palindromeWords = regex({flags: 'gi', plugins: [recursion]})`\b
105122
(?<palindrome>
106123
(?<char> \w )
107-
# Recurse, or match a lone unbalanced char in the center
108124
( \g<palindrome&R=15> | \w? )
109125
\k<char>
110126
)
@@ -114,13 +130,4 @@ const palindromeWords = rregex('gi')`\b
114130
// → ['Racecar', 'ABBA']
115131
```
116132

117-
## Sugar free
118-
119-
Template tag `rregex` is sugar for using the base `regex` tag and adding recursion support via a plugin. You can also add recursion support the verbose way:
120-
121-
```js
122-
import {regex} from 'regex';
123-
import {recursion} from 'regex-recursion';
124-
125-
regex({flags: 'i', plugins: [recursion]})`a(?R=2)?b`;
126-
```
133+
Note the word boundaries (`\b`) at the beginning and end of the regex, outside of the recursive subpattern.

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"browser": "./dist/regex-recursion.min.js",
1010
"scripts": {
1111
"prebuild": "rimraf --glob dist/*",
12-
"build": "esbuild src/index.js --bundle --minify --outfile=dist/regex-recursion.min.js --global-name=Regex.ext",
12+
"build": "esbuild src/index.js --bundle --minify --outfile=dist/regex-recursion.min.js --global-name=Regex.plugins",
1313
"pretest": "npm run build",
1414
"test": "jasmine",
1515
"prepare": "npm test"
@@ -28,10 +28,10 @@
2828
"regexp"
2929
],
3030
"dependencies": {
31-
"regex": "^4.0.0",
3231
"regex-utilities": "^2.1.0"
3332
},
3433
"devDependencies": {
34+
"regex": "^4.0.0",
3535
"esbuild": "^0.23.0",
3636
"jasmine": "^5.2.0",
3737
"rimraf": "^6.0.1"

spec/recursion-spec.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {regex} from 'regex';
2-
import {recursion, rregex} from '../src/index.js';
2+
import {recursion} from '../src/index.js';
33

44
describe('recursion', () => {
55
it('should match an equal number of two different subpatterns', () => {
6-
expect(rregex`a(?R=50)?b`.exec('test aaaaaabbb')[0]).toBe('aaabbb');
6+
expect(regex({plugins: [recursion]})`a(?R=50)?b`.exec('test aaaaaabbb')[0]).toBe('aaabbb');
7+
expect('aAbb').toMatch(regex({flags: 'i', plugins: [recursion]})`a(?R=2)?b`);
78
});
89

910
it('should match an equal number of two different subpatterns, as the entire string', () => {
10-
const re = rregex`^
11+
const re = regex({plugins: [recursion]})`^
1112
(?<balanced>
1213
a
1314
# Recursively match just the specified group
@@ -20,26 +21,26 @@ describe('recursion', () => {
2021
});
2122

2223
it('should match balanced parentheses', () => {
23-
const parens = rregex('g')`\(
24+
const parens = regex({flags: 'g', plugins: [recursion]})`\(
2425
( [^\(\)] | (?R=50) )*
2526
\)`;
2627
expect('test ) (balanced ((parens))) () ((a)) ( (b)'.match(parens)).toEqual(['(balanced ((parens)))', '()', '((a))', '(b)']);
2728
});
2829

2930
it('should match balanced parentheses using an atomic group', () => {
30-
const parens = rregex('g')`\(
31+
const parens = regex({flags: 'g', plugins: [recursion]})`\(
3132
( (?> [^\(\)]+ ) | (?R=50) )*
3233
\)`;
3334
expect('test ) (balanced ((parens))) () ((a)) ( (b)'.match(parens)).toEqual(['(balanced ((parens)))', '()', '((a))', '(b)']);
3435
});
3536

3637
it('should match palindromes', () => {
37-
const palindromes = rregex('gi')`(?<char>\w) ((?R=15)|\w?) \k<char>`;
38+
const palindromes = regex({flags: 'gi', plugins: [recursion]})`(?<char>\w) ((?R=15)|\w?) \k<char>`;
3839
expect('Racecar, ABBA, and redivided'.match(palindromes)).toEqual(['Racecar', 'ABBA', 'edivide']);
3940
});
4041

4142
it('should match palindromes as complete words', () => {
42-
const palindromeWords = rregex('gi')`\b
43+
const palindromeWords = regex({flags: 'gi', plugins: [recursion]})`\b
4344
(?<palindrome>
4445
(?<char> \w )
4546
# Recurse, or match a lone unbalanced char in the center
@@ -51,25 +52,21 @@ describe('recursion', () => {
5152
});
5253

5354
it('should not adjust named backreferences referring outside of the recursed expression', () => {
54-
expect('aababbabcc').toMatch(rregex`^(?<a>a)\k<a>(?<r>(?<b>b)\k<a>\k<b>\k<c>\g<r&R=2>?)(?<c>c)\k<c>$`);
55-
});
56-
57-
it('should allow directly using recursion as a plugin with tag regex', () => {
58-
expect('aAbb').toMatch(regex({flags: 'i', plugins: [recursion]})`a(?R=2)?b`);
55+
expect('aababbabcc').toMatch(regex({plugins: [recursion]})`^(?<a>a)\k<a>(?<r>(?<b>b)\k<a>\k<b>\k<c>\g<r&R=2>?)(?<c>c)\k<c>$`);
5956
});
6057

6158
// Just documenting current behavior; this could be supported in the future
6259
it('should not allow numbered backreferences in interpolated regexes when using recursion', () => {
63-
expect(() => rregex`a(?R=2)?b${/()\1/}`).toThrow();
64-
expect(() => rregex`(?<n>a|\g<n&R=2>${/()\1/})`).toThrow();
65-
expect(() => rregex`(?<n>a|\g<n&R=2>)${/()\1/}`).toThrow();
66-
expect(() => rregex`${/()\1/}a(?R=2)?b`).toThrow();
67-
expect(() => rregex`(?<n>${/()\1/}a|\g<n&R=2>)`).toThrow();
68-
expect(() => rregex`${/()\1/}(?<n>a|\g<n&R=2>)`).toThrow();
60+
expect(() => regex({plugins: [recursion]})`a(?R=2)?b${/()\1/}`).toThrow();
61+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>${/()\1/})`).toThrow();
62+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)${/()\1/}`).toThrow();
63+
expect(() => regex({plugins: [recursion]})`${/()\1/}a(?R=2)?b`).toThrow();
64+
expect(() => regex({plugins: [recursion]})`(?<n>${/()\1/}a|\g<n&R=2>)`).toThrow();
65+
expect(() => regex({plugins: [recursion]})`${/()\1/}(?<n>a|\g<n&R=2>)`).toThrow();
6966
});
7067

71-
it('should not allow definition groups when using recursion', () => {
72-
expect(() => rregex`a(?R=2)?b(?(DEFINE))`).toThrow();
73-
expect(() => rregex`(?<n>a|\g<n&R=2>)(?(DEFINE))`).toThrow();
68+
it('should not allow subroutine definition groups when using recursion', () => {
69+
expect(() => regex({plugins: [recursion]})`a(?R=2)?b(?(DEFINE))`).toThrow();
70+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)(?(DEFINE))`).toThrow();
7471
});
7572
});

src/index.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
1-
import {regex} from 'regex';
21
import {Context, forEachUnescaped, getGroupContents, hasUnescaped, replaceUnescaped} from 'regex-utilities';
32

4-
export function rregex(first, ...values) {
5-
const plugins = (first?.plugins || []).concat(recursion);
6-
// Given a template
7-
if (Array.isArray(first?.raw)) {
8-
return regex({flags: '', plugins})(first, ...values);
9-
// Given flags
10-
} else if ((typeof first === 'string' || first === undefined) && !values.length) {
11-
return regex({flags: first, plugins});
12-
// Given an options object
13-
} else if ({}.toString.call(first) === '[object Object]' && !values.length) {
14-
return regex({...first, plugins});
15-
}
16-
throw new Error(`Unexpected arguments: ${JSON.stringify([first, ...values])}`);
17-
}
18-
193
const gRToken = String.raw`\\g<(?<gRName>[^>&]+)&R=(?<gRDepth>\d+)>`;
204
const recursiveToken = String.raw`\(\?R=(?<rDepth>\d+)\)|${gRToken}`;
215
const namedCapturingDelim = String.raw`\(\?<(?![=!])(?<captureName>[^>]+)>`;

0 commit comments

Comments
 (0)