Skip to content

Commit 2c3baf1

Browse files
committed
Expose utility for normalizePathname behavior
1 parent dcb522f commit 2c3baf1

File tree

3 files changed

+75
-9
lines changed

3 files changed

+75
-9
lines changed

Readme.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@ npm install path-to-regexp --save
1818
## Usage
1919

2020
```javascript
21-
const { pathToRegexp, match, parse, compile } = require("path-to-regexp");
21+
const {
22+
pathToRegexp,
23+
match,
24+
parse,
25+
compile,
26+
normalizePathname
27+
} = require("path-to-regexp");
2228

2329
// pathToRegexp(path, keys?, options?)
2430
// match(path)
2531
// parse(path)
2632
// compile(path)
33+
// normalizePathname(path)
2734
```
2835

2936
- **path** A string, array of strings, or a regular expression.
@@ -162,6 +169,17 @@ match("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
162169
match("/invalid"); //=> false
163170
```
164171

172+
### Normalize Pathname
173+
174+
The `normalizePathname` function will return a normalized string for matching with `pathToRegexp`.
175+
176+
```js
177+
const re = pathToRegexp("/caf\u00E9");
178+
const input = encodeURI("/cafe\u0301");
179+
180+
re.test(normalizePathname(input)); //=> true
181+
```
182+
165183
### Parse
166184

167185
The `parse` function will return a list of strings and keys from a path string:
@@ -231,9 +249,9 @@ Path-To-RegExp exposes the two functions used internally that accept an array of
231249
Path-To-RegExp breaks compatibility with Express <= `4.x`:
232250

233251
- RegExp special characters can only be used in a parameter
234-
- Express.js 4.x used all `RegExp` special characters regardless of position - this considered a bug
252+
- Express.js 4.x supported `RegExp` special characters regardless of position - this is considered a bug
235253
- Parameters have suffixes that augment meaning - `*`, `+` and `?`. E.g. `/:user*`
236-
- No wildcard asterisk (`*`) - use parameters instead (`(.*)`)
254+
- No wildcard asterisk (`*`) - use parameters instead (`(.*)` or `:splat*`)
237255

238256
## Live Demo
239257

src/index.spec.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ type Test = [
55
pathToRegexp.Path,
66
(pathToRegexp.RegExpOptions & pathToRegexp.ParseOptions) | undefined,
77
pathToRegexp.Token[],
8-
Array<[string, (string | undefined)[] | null, pathToRegexp.Match?]>,
8+
Array<
9+
[
10+
string,
11+
(string | undefined)[] | null,
12+
pathToRegexp.Match?,
13+
pathToRegexp.RegexpToFunctionOptions?
14+
]
15+
>,
916
Array<[any, string | null, pathToRegexp.TokensToFunctionOptions?]>
1017
];
1118

@@ -166,6 +173,17 @@ const TESTS: Test[] = [
166173
"/route",
167174
["/route", "route"],
168175
{ path: "/route", index: 0, params: { test: "route" } }
176+
],
177+
[
178+
"/caf%C3%A9",
179+
["/caf%C3%A9", "caf%C3%A9"],
180+
{ path: "/caf%C3%A9", index: 0, params: { test: "caf%C3%A9" } }
181+
],
182+
[
183+
"/caf%C3%A9",
184+
["/caf%C3%A9", "caf%C3%A9"],
185+
{ path: "/caf%C3%A9", index: 0, params: { test: "café" } },
186+
{ decode: decodeURIComponent }
169187
]
170188
],
171189
[
@@ -2732,7 +2750,7 @@ describe("path-to-regexp", function() {
27322750
"match" + (opts ? " using " + util.inspect(opts) : ""),
27332751
function() {
27342752
matchCases.forEach(function(io) {
2735-
const [pathname, matches, params] = io;
2753+
const [pathname, matches, params, options] = io;
27362754
const message = `should ${
27372755
matches ? "" : "not "
27382756
}match ${util.inspect(pathname)}`;
@@ -2742,7 +2760,7 @@ describe("path-to-regexp", function() {
27422760
});
27432761

27442762
if (typeof path === "string" && params !== undefined) {
2745-
const match = pathToRegexp.match(path);
2763+
const match = pathToRegexp.match(path, options);
27462764

27472765
it(message + " params", function() {
27482766
expect(match(pathname)).toEqual(params);
@@ -2802,6 +2820,23 @@ describe("path-to-regexp", function() {
28022820
);
28032821
});
28042822
});
2823+
2824+
describe("normalize pathname", function() {
2825+
it("should match normalized pathnames", function() {
2826+
const re = pathToRegexp.pathToRegexp("/caf\u00E9");
2827+
const input = encodeURI("/cafe\u0301");
2828+
2829+
expect(exec(re, pathToRegexp.normalizePathname(input))).toEqual([
2830+
"/caf\u00E9"
2831+
]);
2832+
});
2833+
2834+
it("should fix repeated slashes", function() {
2835+
const input = encodeURI("/test///route");
2836+
2837+
expect(pathToRegexp.normalizePathname(input)).toEqual("/test/route");
2838+
});
2839+
});
28052840
});
28062841

28072842
/**

src/index.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ export interface ParseOptions {
1414
whitelist?: string | string[];
1515
}
1616

17+
/**
18+
* Normalize a pathname for matching, replaces multiple slashes with a single
19+
* slash and normalizes unicode characters to "NFC". When using this method,
20+
* `decode` should be an identity function so you don't decode strings twice.
21+
*/
22+
export function normalizePathname(pathname: string) {
23+
return decodeURIComponent(pathname)
24+
.replace(/\/+/g, "/")
25+
.normalize();
26+
}
27+
1728
/**
1829
* Balanced bracket helper function.
1930
*/
@@ -46,7 +57,8 @@ function balanced(open: string, close: string, str: string, index: number) {
4657
/**
4758
* Parse a string for the raw tokens.
4859
*/
49-
export function parse(str: string, options: ParseOptions = {}): Token[] {
60+
export function parse(input: string, options: ParseOptions = {}): Token[] {
61+
const str = input.normalize();
5062
const tokens = [];
5163
const defaultDelimiter = options.delimiter ?? DEFAULT_DELIMITER;
5264
const whitelist = options.whitelist ?? undefined;
@@ -97,6 +109,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
97109
if (str[i] === "(") {
98110
const end = balanced("(", ")", str, i);
99111

112+
// False positive on matching brackets.
100113
if (end > -1) {
101114
pattern = str.slice(i + 1, end - 1);
102115
i = end;
@@ -312,7 +325,7 @@ export function match<P extends object = object>(
312325
) {
313326
const keys: Key[] = [];
314327
const re = pathToRegexp(str, keys, options);
315-
return regexpToFunction<P>(re, keys);
328+
return regexpToFunction<P>(re, keys, options);
316329
}
317330

318331
/**
@@ -323,7 +336,7 @@ export function regexpToFunction<P extends object = object>(
323336
keys: Key[],
324337
options: RegexpToFunctionOptions = {}
325338
): MatchFunction<P> {
326-
const { decode = decodeURIComponent } = options;
339+
const { decode = (x: string) => x } = options;
327340

328341
return function(pathname: string) {
329342
const m = re.exec(pathname);

0 commit comments

Comments
 (0)