Skip to content

Commit

Permalink
Rewrite and simplify API
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Sep 1, 2024
1 parent 74f97b5 commit 60f2121
Show file tree
Hide file tree
Showing 8 changed files with 819 additions and 1,767 deletions.
231 changes: 49 additions & 182 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,9 @@ const { match, compile, parse } = require("path-to-regexp");
// parse(path, options?)
```

### Match

The `match` function returns a function for transforming paths into parameters:

- **path** A string.
- **options** _(optional)_ (See [parse](#parse) for more options)
- **sensitive** Regexp will be case sensitive. (default: `false`)
- **end** Validate the match reaches the end of the string. (default: `true`)
- **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`)

```js
const fn = match("/foo/:bar");
```

**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).

### Parameters

Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens.

#### Named parameters

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.
Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. They are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid JavaScript identifier, or be double quoted to use other characters (`:"param-name"`).

```js
const fn = match("/:foo/:bar");
Expand All @@ -55,137 +35,54 @@ fn("/test/route");
//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } }
```

##### Custom matching parameters

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

```js
const exampleNumbers = match("/icon-:foo(\\d+).png");

exampleNumbers("/icon-123.png");
//=> { path: '/icon-123.png', params: { foo: '123' } }

exampleNumbers("/icon-abc.png");
//=> false

const exampleWord = pathToRegexp("/(user|u)");

exampleWord("/u");
//=> { path: '/u', params: { '0': 'u' } }

exampleWord("/users");
//=> false
```

**Tip:** Backslashes need to be escaped with another backslash in JavaScript strings.

#### Unnamed parameters

It is possible to define a parameter without a name. The name will be numerically indexed:

```js
const fn = match("/:foo/(.*)");

fn("/test/route");
//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } }
```

#### Custom prefix and suffix

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

```js
const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?");

fn("/test");
//=> { path: '/test', params: { attr1: 'test' } }

fn("/test-test");
//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } }
```

#### Modifiers

Modifiers are used after parameters with custom prefixes and suffixes (`{}`).

##### Optional

Parameters can be suffixed with a question mark (`?`) to make the parameter optional.

```js
const fn = match("/:foo{/:bar}?");

fn("/test");
//=> { path: '/test', params: { foo: 'test' } }

fn("/test/route");
//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } }
```

##### Zero or more
### Wildcard

Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches.
Wildcard parameters match one or more characters across multiple segments. They are defined the same way as regular parameters, but are prefixed with an asterisk (`*foo`).

```js
const fn = match("{/:foo}*");

fn("/foo");
//=> { path: '/foo', params: { foo: [ 'foo' ] } }
const fn = match("/*splat");

fn("/bar/baz");
//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } }
//=> { path: '/bar/baz', params: { splat: [ 'bar', 'baz' ] } }
```

##### One or more
### Optional

Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches.
Braces can be used to define parts of the path that are optional.

```js
const fn = match("{/:foo}+");
const fn = match("/users{/:id}/delete");

fn("/");
//=> false
fn("/users/delete");
//=> { path: '/users/delete', params: {} }

fn("/bar/baz");
//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } }
fn("/users/123/delete");
//=> { path: '/users/123/delete', params: { id: '123' } }
```

##### Custom separator

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

```js
const fn = match("/name{/:parts;-}+");
## Match

fn("/name");
//=> false
The `match` function returns a function for matching strings against a path:

fn("/bar/1-2-3");
//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } }
```

#### Wildcard

A wildcard is also supported. It is roughly equivalent to `(.*)`.
- **path** String or array of strings.
- **options** _(optional)_ (See [parse](#parse) for more options)
- **sensitive** Regexp will be case sensitive. (default: `false`)
- **end** Validate the match reaches the end of the string. (default: `true`)
- **trailing** Allows optional trailing delimiter to match. (default: `true`)
- **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`)

```js
const fn = match("/*");

fn("/");
//=> { path: '/', params: {} }

fn("/bar/baz");
//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } }
const fn = match("/foo/:bar");
```

### Compile ("Reverse" Path-To-RegExp)
**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).

## Compile ("Reverse" Path-To-RegExp)

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

- **path** A string.
- **options** (See [parse](#parse) for more options)
- **sensitive** Regexp will be case sensitive. (default: `false`)
- **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`)
- **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`)

```js
Expand All @@ -194,46 +91,34 @@ const toPath = compile("/user/:id");
toPath({ id: "name" }); //=> "/user/name"
toPath({ id: "café" }); //=> "/user/caf%C3%A9"

// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted.
const toPathRaw = compile("/user/:id", { encode: false });

toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F"
toPathRaw({ id: ":/" }); //=> Throws, "/user/:/" when `validate` is `false`.

const toPathRepeated = compile("{/:segment}+");
const toPathRepeated = compile("/*segment");

toPathRepeated({ segment: ["foo"] }); //=> "/foo"
toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c"

const toPathRegexp = compile("/user/:id(\\d+)");
// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted.
const toPathRaw = compile("/user/:id", { encode: false });

toPathRegexp({ id: "123" }); //=> "/user/123"
toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F"
```

## Developers

- If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around.
- To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`.
- To ensure matches work on paths containing characters usually encoded, such as emoji, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`.

### Parse

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`.
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`.

- **path** A string.
- **options** _(optional)_
- **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`)
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding)
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl))

### Tokens

The `tokens` returned by `TokenData` is an array of strings or keys, represented as objects, with the following properties:

- `name` The name of the token
- `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`)
- `suffix` _(optional)_ The suffix string for the segment (e.g. `""`)
- `pattern` _(optional)_ The pattern defined to match this token
- `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`)
- `separator` _(optional)_ The string used to separate repeated parameters
`TokenData` is a sequence of tokens, currently of types `text`, `parameter`, `wildcard`, or `group`.

### Custom path

Expand All @@ -242,9 +127,12 @@ In some applications, you may not be able to use the `path-to-regexp` syntax, bu
```js
import { TokenData, match } from "path-to-regexp";

const tokens = ["/", { name: "foo" }];
const path = new TokenData(tokens, "/");
const fn = $match(path);
const tokens = [
{ type: "text", value: "/" },
{ type: "parameter", name: "foo" },
];
const path = new TokenData(tokens);
const fn = match(path);

fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } }
```
Expand All @@ -253,55 +141,34 @@ fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } }

An effort has been made to ensure ambiguous paths from previous releases throw an error. This means you might be seeing an error when things worked before.

### Unexpected `?`, `*`, or `+`

In previous major versions `/` and `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. For example:

- `/:key?``{/:key}?` or `/:key*``{/:key}*` or `/:key+``{/:key}+`
- `.:key?``{.:key}?` or `.:key*``{.:key}*` or `.:key+``{.:key}+`
- `:key?``{:key}?` or `:key*``{:key}*` or `:key+``{:key}+`
### Unexpected `?` or `+`

### Unexpected `;`
In past releases, `?`, `*`, and `+` were used to denote optional or repeating parameters. As an alternative, try these:

Used as a [custom separator](#custom-separator) for repeated parameters.
- For optional (`?`), use an empty segment in a group such as `/:file{.:ext}`.
- For repeating (`+`), only wildcard matching is supported, such as `/*path`.
- For optional repeating (`*`), use a group and a wildcard parameter such as `/files{/*path}`.

### Unexpected `!`, `@`, or `,`
### Unexpected `(`, `)`, `[`, `]`, etc.

These characters have been reserved for future use.

### Missing separator

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}*`.
Previous versions of Path-to-RegExp used these for RegExp features. This version no longer supports them so they've been reserved to avoid ambiguity. To use these characters literally, escape them with a backslash, e.g. `"\\("`.

### Missing parameter name

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"`.
Parameter names, the part after `:` or `*`, must be a valid JavaScript identifier. For example, it cannot start with a number or contain a dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`.

### Unterminated quote

Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character.

### Pattern cannot start with "?"

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.

### Capturing groups are not allowed

A parameter pattern can not contain nested capturing groups.

### Unbalanced or missing pattern

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.

### Express <= 4.x

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

- The only part of the string that is a regex is within `()`.
- In Express.js 4.x, everything was passed as-is after a simple replacement, so you could write `/[a-z]+` to match `/test`.
- The `?` optional character must be used after `{}`.
- Regexp characters can no longer be provided.
- The optional character `?` is no longer supported, use braces instead: `/:file{.:ext}`.
- Some characters have new meaning or have been reserved (`{}?*+@!;`).
- The parameter name now supports all unicode identifier characters, previously it was only `[a-z0-9]`.
- The parameter name now supports all JavaScript identifier characters, previously it was only `[a-z0-9]`.

## License

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dist/"
],
"scripts": {
"bench": "vitest bench",
"build": "ts-scripts build",
"format": "ts-scripts format",
"lint": "ts-scripts lint",
Expand Down
8 changes: 1 addition & 7 deletions scripts/redos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import { MATCH_TESTS } from "../src/cases.spec.js";
let safe = 0;
let fail = 0;

const TESTS = new Set(MATCH_TESTS.map((test) => test.path));
// const TESTS = [
// ":path([^\\.]+).:ext",
// ":path.:ext(\\w+)",
// ":path{.:ext([^\\.]+)}",
// "/:path.:ext(\\\\w+)",
// ];
const TESTS = MATCH_TESTS.map((x) => x.path);

for (const path of TESTS) {
const { re } = match(path) as any;
Expand Down
Loading

0 comments on commit 60f2121

Please sign in to comment.