Skip to content

Commit 208cc83

Browse files
committed
Refactor params to match up to next token
1 parent cbf2c73 commit 208cc83

File tree

5 files changed

+584
-1424
lines changed

5 files changed

+584
-1424
lines changed

Readme.md

Lines changed: 94 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -17,75 +17,64 @@ npm install path-to-regexp --save
1717
## Usage
1818

1919
```js
20-
const { pathToRegexp, match, parse, compile } = require("path-to-regexp");
20+
const { match, compile, parse } = require("path-to-regexp");
2121

22-
// pathToRegexp(path, options?)
2322
// match(path, options?)
24-
// parse(path, options?)
2523
// compile(path, options?)
24+
// parse(path, options?)
2625
```
2726

28-
### Path to regexp
27+
### Match
2928

30-
The `pathToRegexp` function returns a regular expression with `keys` as a property. It accepts the following arguments:
29+
The `match` function returns a function for transforming paths into parameters:
3130

3231
- **path** A string.
33-
- **options** _(optional)_
32+
- **options** _(optional)_ (See [parse](#parse) for more options)
3433
- **sensitive** Regexp will be case sensitive. (default: `false`)
35-
- **trailing** Allows optional trailing delimiter to match. (default: `true`)
36-
- **strict** Verify patterns are valid and safe to use. (default: `false`, recommended: `true`)
37-
- **end** Match to the end of the string. (default: `true`)
38-
- **start** Match from the beginning of the string. (default: `true`)
39-
- **loose** Allow the delimiter to be arbitrarily repeated, e.g. `/` or `///`. (default: `true`)
40-
- **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`)
41-
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding)
34+
- **end** Validate the match reaches the end of the string. (default: `true`)
35+
- **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`)
4236

4337
```js
44-
const regexp = pathToRegexp("/foo/:bar");
45-
// regexp = /^\/+foo(?:\/+([^\/]+?))(?:\/+)?$/i
46-
// keys = [{ name: 'bar', prefix: '', suffix: '', pattern: '', modifier: '' }]
38+
const fn = match("/foo/:bar");
4739
```
4840

49-
**Please note:** The `RegExp` returned by `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc).
41+
**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc).
5042

5143
### Parameters
5244

53-
The path argument is used to define parameters and populate keys.
45+
Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens.
5446

5547
#### Named parameters
5648

57-
Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters (similar to JavaScript).
49+
Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters, similar to JavaScript.
5850

5951
```js
60-
const regexp = pathToRegexp("/:foo/:bar");
61-
// keys = [{ name: 'foo', ... }, { name: 'bar', ... }]
52+
const fn = match("/:foo/:bar");
6253

63-
regexp.exec("/test/route");
64-
//=> [ '/test/route', 'test', 'route', index: 0 ]
54+
fn("/test/route");
55+
//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } }
6556
```
6657

6758
##### Custom matching parameters
6859

6960
Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path:
7061

7162
```js
72-
const regexpNumbers = pathToRegexp("/icon-:foo(\\d+).png");
73-
// keys = [{ name: 'foo', ... }]
63+
const exampleNumbers = match("/icon-:foo(\\d+).png");
7464

75-
regexpNumbers.exec("/icon-123.png");
76-
//=> ['/icon-123.png', '123']
65+
exampleNumbers("/icon-123.png");
66+
//=> { path: '/icon-123.png', params: { foo: '123' } }
7767

78-
regexpNumbers.exec("/icon-abc.png");
79-
//=> null
68+
exampleNumbers("/icon-abc.png");
69+
//=> false
8070

81-
const regexpWord = pathToRegexp("/(user|u)");
82-
// keys = [{ name: 0, ... }]
71+
const exampleWord = pathToRegexp("/(user|u)");
8372

84-
regexpWord.exec("/u");
85-
//=> ['/u', 'u']
73+
exampleWord("/u");
74+
//=> { path: '/u', params: { '0': 'u' } }
8675

87-
regexpWord.exec("/users");
88-
//=> null
76+
exampleWord("/users");
77+
//=> false
8978
```
9079

9180
**Tip:** Backslashes need to be escaped with another backslash in JavaScript strings.
@@ -95,25 +84,24 @@ regexpWord.exec("/users");
9584
It is possible to define a parameter without a name. The name will be numerically indexed:
9685

9786
```js
98-
const regexp = pathToRegexp("/:foo/(.*)");
99-
// keys = [{ name: 'foo', ... }, { name: '0', ... }]
87+
const fn = match("/:foo/(.*)");
10088

101-
regexp.exec("/test/route");
102-
//=> [ '/test/route', 'test', 'route', index: 0 ]
89+
fn("/test/route");
90+
//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } }
10391
```
10492

105-
##### Custom prefix and suffix
93+
#### Custom prefix and suffix
10694

10795
Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment:
10896

10997
```js
110-
const regexp = pathToRegexp("{/:attr1}?{-:attr2}?{-:attr3}?");
98+
const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?");
11199

112-
regexp.exec("/test");
113-
// => ['/test', 'test', undefined, undefined]
100+
fn("/test");
101+
//=> { path: '/test', params: { attr1: 'test' } }
114102

115-
regexp.exec("/test-test");
116-
// => ['/test', 'test', 'test', undefined]
103+
fn("/test-test");
104+
//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } }
117105
```
118106

119107
#### Modifiers
@@ -125,99 +113,78 @@ Modifiers are used after parameters with custom prefixes and suffixes (`{}`).
125113
Parameters can be suffixed with a question mark (`?`) to make the parameter optional.
126114

127115
```js
128-
const regexp = pathToRegexp("/:foo{/:bar}?");
129-
// keys = [{ name: 'foo', ... }, { name: 'bar', prefix: '/', modifier: '?' }]
116+
const fn = match("/:foo{/:bar}?");
130117

131-
regexp.exec("/test");
132-
//=> [ '/test', 'test', undefined, index: 0 ]
118+
fn("/test");
119+
//=> { path: '/test', params: { foo: 'test' } }
133120

134-
regexp.exec("/test/route");
135-
//=> [ '/test/route', 'test', 'route', index: 0 ]
121+
fn("/test/route");
122+
//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } }
136123
```
137124

138125
##### Zero or more
139126

140127
Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches.
141128

142129
```js
143-
const regexp = pathToRegexp("{/:foo}*");
144-
// keys = [{ name: 'foo', prefix: '/', modifier: '*' }]
130+
const fn = match("{/:foo}*");
145131

146-
regexp.exec("/foo");
147-
//=> [ '/foo', "foo", index: 0 ]
132+
fn("/foo");
133+
//=> { path: '/foo', params: { foo: [ 'foo' ] } }
148134

149-
regexp.exec("/bar/baz");
150-
//=> [ '/bar/baz', 'bar/baz', index: 0 ]
135+
fn("/bar/baz");
136+
//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } }
151137
```
152138

153139
##### One or more
154140

155141
Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches.
156142

157143
```js
158-
const regexp = pathToRegexp("{/:foo}+");
159-
// keys = [{ name: 'foo', prefix: '/', modifier: '+' }]
144+
const fn = match("{/:foo}+");
160145

161-
regexp.exec("/");
162-
//=> null
146+
fn("/");
147+
//=> false
163148

164-
regexp.exec("/bar/baz");
165-
//=> [ '/bar/baz', 'bar/baz', index: 0 ]
149+
fn("/bar/baz");
150+
//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } }
166151
```
167152

168153
##### Custom separator
169154

170155
By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this:
171156

172157
```js
173-
const regexp = pathToRegexp("/name{/:parts;-}+");
158+
const fn = match("/name{/:parts;-}+");
174159

175-
regexp.exec("/name");
176-
//=> null
160+
fn("/name");
161+
//=> false
177162

178-
regexp.exec("/bar/1-2-3");
179-
//=> [ '/name/1-2-3', '1-2-3', index: 0 ]
163+
fn("/bar/1-2-3");
164+
//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } }
180165
```
181166

182167
#### Wildcard
183168

184-
A wildcard can also be used. It is roughly equivalent to `(.*)`.
169+
A wildcard is also supported. It is roughly equivalent to `(.*)`.
185170

186171
```js
187-
const regexp = pathToRegexp("/*");
188-
// keys = [{ name: '0', pattern: '[^\\/]*', separator: '/', modifier: '*' }]
189-
190-
regexp.exec("/");
191-
//=> [ '/', '', index: 0 ]
192-
193-
regexp.exec("/bar/baz");
194-
//=> [ '/bar/baz', 'bar/baz', index: 0 ]
195-
```
172+
const fn = match("/*");
196173

197-
### Match
174+
fn("/");
175+
//=> { path: '/', params: {} }
198176

199-
The `match` function returns a function for transforming paths into parameters:
200-
201-
- **path** A string.
202-
- **options** _(optional)_ The same options as `pathToRegexp`, plus:
203-
- **decode** Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`)
204-
205-
```js
206-
const fn = match("/user/:id");
207-
208-
fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
209-
fn("/invalid"); //=> false
210-
fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } }
177+
fn("/bar/baz");
178+
//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } }
211179
```
212180

213-
**Note:** Setting `decode: false` disables the "splitting" behavior of repeated parameters, which is useful if you need the exactly matched parameter back.
214-
215181
### Compile ("Reverse" Path-To-RegExp)
216182

217183
The `compile` function will return a function for transforming parameters into a valid path:
218184

219185
- **path** A string.
220-
- **options** _(optional)_ Similar to `pathToRegexp` (`delimiter`, `encodePath`, `sensitive`, and `loose`), plus:
186+
- **options** (See [parse](#parse) for more options)
187+
- **sensitive** Regexp will be case sensitive. (default: `false`)
221188
- **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`)
222189
- **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`)
223190

@@ -245,14 +212,17 @@ toPathRegexp({ id: "123" }); //=> "/user/123"
245212

246213
## Developers
247214

248-
- If you are rewriting paths with match and compiler, consider using `encode: false` and `decode: false` to keep raw paths passed around.
215+
- If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around.
249216
- To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`.
250-
- If matches are intended to be exact, you need to set `loose: false`, `trailing: false`, and `sensitive: true`.
251-
- Enable `strict: true` to detect ReDOS issues.
252217

253218
### Parse
254219

255-
A `parse` function is available and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can passed directly into `pathToRegexp`, `match`, and `compile`. It accepts only two options, `delimiter` and `encodePath`, which makes those options redundant in the above methods.
220+
The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `$match` and `$compile`.
221+
222+
- **path** A string.
223+
- **options** _(optional)_
224+
- **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`)
225+
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding)
256226

257227
### Tokens
258228

@@ -267,14 +237,14 @@ The `tokens` returned by `TokenData` is an array of strings or keys, represented
267237

268238
### Custom path
269239

270-
In some applications, you may not be able to use the `path-to-regexp` syntax (e.g. file-based routing), but you can still use this library for `match`, `compile`, and `pathToRegexp` by building your own `TokenData` instance. For example:
240+
In some applications, you may not be able to use the `path-to-regexp` syntax, but still want to use this library for `match` and `compile`. For example:
271241

272242
```js
273243
import { TokenData, match } from "path-to-regexp";
274244

275245
const tokens = ["/", { name: "foo" }];
276246
const path = new TokenData(tokens, "/");
277-
const fn = match(path);
247+
const fn = $match(path);
278248

279249
fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } }
280250
```
@@ -299,6 +269,30 @@ Used as a [custom separator](#custom-separator) for repeated parameters.
299269

300270
These characters have been reserved for future use.
301271

272+
### Missing separator
273+
274+
Repeated parameters must have a separator to be valid. For example, `{:foo}*` can't be used. Separators can be defined manually, such as `{:foo;/}*`, or they default to the suffix and prefix with the parameter, such as `{/:foo}*`.
275+
276+
### Missing parameter name
277+
278+
Parameter names, the part after `:`, must be a valid JavaScript identifier. For example, it cannot start with a number or dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`.
279+
280+
### Unterminated quote
281+
282+
Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character.
283+
284+
### Pattern cannot start with "?"
285+
286+
Parameters in `path-to-regexp` must be basic groups. However, you can use features that require the `?` nested within the pattern. For example, `:foo((?!login)[^/]+)` is valid, but `:foo(?!login)` is not.
287+
288+
### Capturing groups are not allowed
289+
290+
A parameter pattern can not contain nested capturing groups.
291+
292+
### Unbalanced or missing pattern
293+
294+
A parameter pattern must have the expected number of parentheses. An unbalanced amount, such as `((?!login)` implies something has been written that is invalid. Check you didn't forget any parentheses.
295+
302296
### Express <= 4.x
303297

304298
Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways:

scripts/redos.ts

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
11
import { checkSync } from "recheck";
2-
import { pathToRegexp } from "../src/index.js";
2+
import { match } from "../src/index.js";
3+
import { MATCH_TESTS } from "../src/cases.spec.js";
34

4-
const TESTS = [
5-
"/abc{abc:foo}?",
6-
"/:foo{abc:foo}?",
7-
"{:attr1}?{:attr2/}?",
8-
"{:attr1/}?{:attr2/}?",
9-
"{:foo.}?{:bar.}?",
10-
"{:foo([^\\.]+).}?{:bar.}?",
11-
":foo(a+):bar(b+)",
12-
];
5+
let safe = 0;
6+
let fail = 0;
7+
8+
const TESTS = new Set(MATCH_TESTS.map((test) => test.path));
9+
// const TESTS = [
10+
// ":path([^\\.]+).:ext",
11+
// ":path.:ext(\\w+)",
12+
// ":path{.:ext([^\\.]+)}",
13+
// "/:path.:ext(\\\\w+)",
14+
// ];
1315

1416
for (const path of TESTS) {
15-
try {
16-
const re = pathToRegexp(path, { strict: true });
17-
const result = checkSync(re.source, re.flags);
18-
if (result.status === "safe") {
19-
console.log("Safe:", path, String(re));
20-
} else {
21-
console.log("Fail:", path, String(re));
22-
}
23-
} catch (err) {
24-
try {
25-
const re = pathToRegexp(path);
26-
const result = checkSync(re.source, re.flags);
27-
if (result.status === "safe") {
28-
console.log("Invalid:", path, String(re));
29-
} else {
30-
console.log("Pass:", path, String(re));
31-
}
32-
} catch (err) {
33-
console.log("Error:", path, err.message);
34-
}
17+
const { re } = match(path);
18+
const result = checkSync(re.source, re.flags);
19+
if (result.status === "safe") {
20+
safe++;
21+
console.log("Safe:", path, String(re));
22+
} else {
23+
fail++;
24+
console.log("Fail:", path, String(re));
3525
}
3626
}
27+
28+
console.log("Safe:", safe, "Fail:", fail);

0 commit comments

Comments
 (0)