Skip to content

Commit 0702846

Browse files
committed
Add support for + and * parameter suffixes
* Updated readme to reflect changes * Added edge case handling for trailing slashes in non-strict mode when the path passed in already ends in a slash * Tests
1 parent f977986 commit 0702846

File tree

3 files changed

+93
-25
lines changed

3 files changed

+93
-25
lines changed

Readme.md

+38-5
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,21 @@ The path has the ability to define parameters and automatically populate the key
3636
Named parameters are defined by prefixing a colon to the parameter name (`:foo`). By default, this parameter will match up to the next path segment.
3737

3838
```js
39-
var re = pathToRegexp('/:foo/:bar');
39+
var re = pathToRegexp('/:foo/:bar', keys);
4040
// keys = ['foo', 'bar']
4141

4242
re.exec('/test/route');
4343
//=> ['/test/route', 'test', 'route']
4444
```
4545

46-
#### Optional Parameters
46+
#### Parameter Suffixes
4747

48-
Optional parameters can be denoted by suffixing a question mark. This will also make any prefixed path segment optional (`/` or `.`).
48+
##### Optional
49+
50+
Parameters can be suffixed with a question mark (`?`) to make the entire parameter optional. This will also make any prefixed path delimiter optional (`/` or `.`).
4951

5052
```js
51-
var re = pathToRegexp('/:foo/:bar?');
53+
var re = pathToRegexp('/:foo/:bar?', keys);
5254
// keys = ['foo', 'bar']
5355

5456
re.exec('/test');
@@ -58,12 +60,43 @@ re.exec('/test/route');
5860
//=> ['/test', 'test', 'route']
5961
```
6062

63+
##### Zero or more
64+
65+
Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter match. The prefixed path delimiter is also taken into account for the match.
66+
67+
```js
68+
var re = pathToRegexp('/:foo*', keys);
69+
// keys = ['foo']
70+
71+
re.exec('/');
72+
//=> ['/', undefined]
73+
74+
re.exec('/bar/baz');
75+
//=> ['/bar/baz', 'bar/baz']
76+
```
77+
78+
##### One or more
79+
80+
Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameters match. The prefixed path delimiter is included in the match.
81+
82+
```js
83+
var re = pathToRegexp('/:foo+', keys);
84+
// keys = ['foo']
85+
86+
re.exec('/');
87+
//=> null
88+
89+
re.exec('/bar/baz');
90+
//=> ['/bar/baz', 'bar/baz']
91+
```
92+
6193
#### Custom Matches
6294

6395
All parameters can be provided a custom matching regexp and override the default. Please note: Backslashes need to be escaped in strings.
6496

6597
```js
66-
var re = pathToRegexp('/:foo(\\d+)');
98+
var re = pathToRegexp('/:foo(\\d+)', keys);
99+
// keys = ['foo']
67100

68101
re.exec('/123');
69102
//=> ['/123', '123']

index.js

+31-20
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var PATH_REGEXP = new RegExp([
1313
//
1414
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
1515
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
16-
'([\\/.])?(?:\\:(\\w+)(?:\\((.+)\\))?|\\((.+)\\))([?])?',
16+
'([\\/.])?(?:\\:(\\w+)(?:\\((.+)\\))?|\\((.+)\\))([+*?])?',
1717
// Match regexp special characters that should always be escaped.
1818
'([.+*?=^!:${}()[\\]|\\/])'
1919
].join('|'), 'g');
@@ -24,8 +24,8 @@ var PATH_REGEXP = new RegExp([
2424
* @param {String} group
2525
* @return {String}
2626
*/
27-
function wrapGroup (group) {
28-
return '(' + group.replace(/([=!:$\/()])/g, '\\$1') + ')';
27+
function escapeGroup (group) {
28+
return group.replace(/([=!:$\/()])/g, '\\$1');
2929
}
3030

3131
/**
@@ -72,7 +72,7 @@ function pathtoRegexp (path, keys, options) {
7272
}
7373

7474
// Alter the path string into a usable regexp.
75-
path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, optional, escape) {
75+
path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape) {
7676
// Avoiding re-escaping escaped characters.
7777
if (escaped) {
7878
return escaped;
@@ -88,33 +88,44 @@ function pathtoRegexp (path, keys, options) {
8888
// Special behaviour for format params.
8989
var format = prefix === '.' ? '\\.' : '';
9090

91-
// Escape the prefix and ensure both optional matches are empty strings.
91+
// Escape the prefix character.
9292
prefix = prefix ? '\\' + prefix : '';
93-
optional = optional || '';
9493

9594
// Match using the custom capturing group, or fallback to capturing
9695
// everything up to the next slash (or next period if the param was
9796
// prefixed with a period).
98-
var regexp = wrapGroup(capture || group || '[^\\/' + format + ']+?');
97+
capture = escapeGroup(capture || group || '[^\\/' + format + ']+?');
9998

100-
if (optional) {
101-
return '(?:' + prefix + regexp + ')' + optional;
99+
// More complex regexp is required for suffix support.
100+
if (suffix) {
101+
if (suffix === '+') {
102+
return prefix + '(' + capture + '(?:' + prefix + capture + ')*)'
103+
}
104+
105+
if (suffix === '*') {
106+
return '(?:' + prefix + '(' + capture + '(?:' + prefix + capture + ')*|' + capture + '))?';
107+
}
108+
109+
return '(?:' + prefix + '(' + capture + '))?';
102110
}
103111

104-
return prefix + regexp;
112+
// Basic parameter support.
113+
return prefix + '(' + capture + ')';
105114
});
106115

107-
// If we are doing a non-ending match, we need to prompt the matching groups
108-
// to match as much as possible. To do this, we add a positive lookahead for
109-
// the next path fragment or the end. However, if the regexp already ends
110-
// in a path fragment, we'll run into problems.
111-
if (!end && path[path.length - 1] !== '/') {
112-
path += '(?=\\/|$)';
113-
}
116+
if (path[path.length - 1] !== '/') {
117+
// If we are doing a non-ending match, we need to prompt the matching groups
118+
// to match as much as possible. To do this, we add a positive lookahead for
119+
// the next path fragment or the end. However, if the regexp already ends
120+
// in a path fragment, we'll run into problems.
121+
if (!end) {
122+
path += '(?=\\/|$)';
123+
}
114124

115-
// Allow trailing slashes to be matched in non-strict, ending mode.
116-
if (end && !strict) {
117-
path += '\\/?';
125+
// Allow trailing slashes to be matched in non-strict, ending mode.
126+
if (end && !strict) {
127+
path += '\\/?';
128+
}
118129
}
119130

120131
return new RegExp('^' + path + (end ? '$' : ''), flags);

test.js

+24
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ var exec = function (re, str) {
2525
*/
2626
var TESTS = [
2727
// Simple paths.
28+
['/', [], '/', ['/']],
2829
['/test', [], '/test', ['/test']],
2930
['/test', [], '/route', null],
3031
['/test', [], '/test/route', null],
3132
['/test', [], '/test/', ['/test/']],
33+
['/test/', [], '/test/', ['/test/']],
34+
['/test/', [], '/test//', null],
3235

3336
// Case-sensitive paths.
3437
['/test', [], '/test', ['/test'], { sensitive: true }],
@@ -84,8 +87,29 @@ var TESTS = [
8487
['/:test?', ['test'], '/route', ['/route', 'route'], { strict: true }],
8588
['/:test?', ['test'], '/', null, { strict: true }], // Questionable behaviour.
8689
['/:test?/', ['test'], '/', ['/', undefined], { strict: true }],
90+
['/:test?/', ['test'], '//', null],
8791
['/:test?/', ['test'], '//', null, { strict: true }],
8892

93+
// Repeated once or more times parameters.
94+
['/:test+', ['test'], '/', null],
95+
['/:test+', ['test'], '/route', ['/route', 'route']],
96+
['/:test+', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']],
97+
['/:test(\\d+)+', ['test'], '/abc/456/789', null],
98+
['/:test(\\d+)+', ['test'], '/123/456/789', ['/123/456/789', '123/456/789']],
99+
['/route.:ext(json|xml)+', ['ext'], '/route.json', ['/route.json', 'json']],
100+
['/route.:ext(json|xml)+', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']],
101+
['/route.:ext(json|xml)+', ['ext'], '/route.html', null],
102+
103+
// Repeated zero or more times parameters.
104+
['/:test*', ['test'], '/', ['/', undefined]],
105+
['/:test*', ['test'], '//', null],
106+
['/:test*', ['test'], '/route', ['/route', 'route']],
107+
['/:test*', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']],
108+
['/route.:ext([a-z]+)*', ['ext'], '/route', ['/route', undefined]],
109+
['/route.:ext([a-z]+)*', ['ext'], '/route.json', ['/route.json', 'json']],
110+
['/route.:ext([a-z]+)*', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']],
111+
['/route.:ext([a-z]+)*', ['ext'], '/route.123', null],
112+
89113
// Custom named parameters.
90114
['/:test(\\d+)', ['test'], '/123', ['/123', '123']],
91115
['/:test(\\d+)', ['test'], '/abc', null],

0 commit comments

Comments
 (0)