diff --git a/Readme.md b/Readme.md index dc267b7..eaa60b2 100644 --- a/Readme.md +++ b/Readme.md @@ -17,51 +17,42 @@ npm install path-to-regexp --save ## Usage ```js -const { pathToRegexp, match, parse, compile } = require("path-to-regexp"); +const { match, compile, parse } = require("path-to-regexp"); -// pathToRegexp(path, options?) // match(path, options?) -// parse(path, options?) // compile(path, options?) +// parse(path, options?) ``` -### Path to regexp +### Match -The `pathToRegexp` function returns a regular expression with `keys` as a property. It accepts the following arguments: +The `match` function returns a function for transforming paths into parameters: - **path** A string. -- **options** _(optional)_ +- **options** _(optional)_ (See [parse](#parse) for more options) - **sensitive** Regexp will be case sensitive. (default: `false`) - - **trailing** Allows optional trailing delimiter to match. (default: `true`) - - **strict** Verify patterns are valid and safe to use. (default: `false`, recommended: `true`) - - **end** Match to the end of the string. (default: `true`) - - **start** Match from the beginning of the string. (default: `true`) - - **loose** Allow the delimiter to be arbitrarily repeated, e.g. `/` or `///`. (default: `true`) - - **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) + - **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 regexp = pathToRegexp("/foo/:bar"); -// regexp = /^\/+foo(?:\/+([^\/]+?))(?:\/+)?$/i -// keys = [{ name: 'bar', prefix: '', suffix: '', pattern: '', modifier: '' }] +const fn = match("/foo/:bar"); ``` -**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). +**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 -The path argument is used to define parameters and populate keys. +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). +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. ```js -const regexp = pathToRegexp("/:foo/:bar"); -// keys = [{ name: 'foo', ... }, { name: 'bar', ... }] +const fn = match("/:foo/:bar"); -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` ##### Custom matching parameters @@ -69,23 +60,21 @@ regexp.exec("/test/route"); Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: ```js -const regexpNumbers = pathToRegexp("/icon-:foo(\\d+).png"); -// keys = [{ name: 'foo', ... }] +const exampleNumbers = match("/icon-:foo(\\d+).png"); -regexpNumbers.exec("/icon-123.png"); -//=> ['/icon-123.png', '123'] +exampleNumbers("/icon-123.png"); +//=> { path: '/icon-123.png', params: { foo: '123' } } -regexpNumbers.exec("/icon-abc.png"); -//=> null +exampleNumbers("/icon-abc.png"); +//=> false -const regexpWord = pathToRegexp("/(user|u)"); -// keys = [{ name: 0, ... }] +const exampleWord = pathToRegexp("/(user|u)"); -regexpWord.exec("/u"); -//=> ['/u', 'u'] +exampleWord("/u"); +//=> { path: '/u', params: { '0': 'u' } } -regexpWord.exec("/users"); -//=> null +exampleWord("/users"); +//=> false ``` **Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. @@ -95,25 +84,24 @@ regexpWord.exec("/users"); It is possible to define a parameter without a name. The name will be numerically indexed: ```js -const regexp = pathToRegexp("/:foo/(.*)"); -// keys = [{ name: 'foo', ... }, { name: '0', ... }] +const fn = match("/:foo/(.*)"); -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } } ``` -##### Custom prefix and suffix +#### Custom prefix and suffix Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: ```js -const regexp = pathToRegexp("{/:attr1}?{-:attr2}?{-:attr3}?"); +const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?"); -regexp.exec("/test"); -// => ['/test', 'test', undefined, undefined] +fn("/test"); +//=> { path: '/test', params: { attr1: 'test' } } -regexp.exec("/test-test"); -// => ['/test', 'test', 'test', undefined] +fn("/test-test"); +//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } } ``` #### Modifiers @@ -125,14 +113,13 @@ Modifiers are used after parameters with custom prefixes and suffixes (`{}`). Parameters can be suffixed with a question mark (`?`) to make the parameter optional. ```js -const regexp = pathToRegexp("/:foo{/:bar}?"); -// keys = [{ name: 'foo', ... }, { name: 'bar', prefix: '/', modifier: '?' }] +const fn = match("/:foo{/:bar}?"); -regexp.exec("/test"); -//=> [ '/test', 'test', undefined, index: 0 ] +fn("/test"); +//=> { path: '/test', params: { foo: 'test' } } -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` ##### Zero or more @@ -140,14 +127,13 @@ regexp.exec("/test/route"); Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. ```js -const regexp = pathToRegexp("{/:foo}*"); -// keys = [{ name: 'foo', prefix: '/', modifier: '*' }] +const fn = match("{/:foo}*"); -regexp.exec("/foo"); -//=> [ '/foo', "foo", index: 0 ] +fn("/foo"); +//=> { path: '/foo', params: { foo: [ 'foo' ] } } -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } ``` ##### One or more @@ -155,14 +141,13 @@ regexp.exec("/bar/baz"); Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches. ```js -const regexp = pathToRegexp("{/:foo}+"); -// keys = [{ name: 'foo', prefix: '/', modifier: '+' }] +const fn = match("{/:foo}+"); -regexp.exec("/"); -//=> null +fn("/"); +//=> false -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } ``` ##### Custom separator @@ -170,54 +155,36 @@ regexp.exec("/bar/baz"); By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: ```js -const regexp = pathToRegexp("/name{/:parts;-}+"); +const fn = match("/name{/:parts;-}+"); -regexp.exec("/name"); -//=> null +fn("/name"); +//=> false -regexp.exec("/bar/1-2-3"); -//=> [ '/name/1-2-3', '1-2-3', index: 0 ] +fn("/bar/1-2-3"); +//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } } ``` #### Wildcard -A wildcard can also be used. It is roughly equivalent to `(.*)`. +A wildcard is also supported. It is roughly equivalent to `(.*)`. ```js -const regexp = pathToRegexp("/*"); -// keys = [{ name: '0', pattern: '[^\\/]*', separator: '/', modifier: '*' }] - -regexp.exec("/"); -//=> [ '/', '', index: 0 ] - -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] -``` +const fn = match("/*"); -### Match +fn("/"); +//=> { path: '/', params: {} } -The `match` function returns a function for transforming paths into parameters: - -- **path** A string. -- **options** _(optional)_ The same options as `pathToRegexp`, plus: - - **decode** Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) - -```js -const fn = match("/user/:id"); - -fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } } -fn("/invalid"); //=> false -fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } } +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } } ``` -**Note:** Setting `decode: false` disables the "splitting" behavior of repeated parameters, which is useful if you need the exactly matched parameter back. - ### Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: - **path** A string. -- **options** _(optional)_ Similar to `pathToRegexp` (`delimiter`, `encodePath`, `sensitive`, and `loose`), plus: +- **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`) @@ -245,14 +212,17 @@ toPathRegexp({ id: "123" }); //=> "/user/123" ## Developers -- If you are rewriting paths with match and compiler, consider using `encode: false` and `decode: false` to keep raw paths passed around. +- 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`. -- If matches are intended to be exact, you need to set `loose: false`, `trailing: false`, and `sensitive: true`. -- Enable `strict: true` to detect ReDOS issues. ### Parse -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. +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) ### Tokens @@ -267,14 +237,14 @@ The `tokens` returned by `TokenData` is an array of strings or keys, represented ### Custom path -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: +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: ```js import { TokenData, match } from "path-to-regexp"; const tokens = ["/", { name: "foo" }]; const path = new TokenData(tokens, "/"); -const fn = match(path); +const fn = $match(path); fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } ``` @@ -299,6 +269,30 @@ Used as a [custom separator](#custom-separator) for repeated parameters. 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}*`. + +### 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"`. + +### 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: diff --git a/scripts/redos.ts b/scripts/redos.ts index c675e71..fe2c7ac 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -1,36 +1,28 @@ import { checkSync } from "recheck"; -import { pathToRegexp } from "../src/index.js"; +import { match } from "../src/index.js"; +import { MATCH_TESTS } from "../src/cases.spec.js"; -const TESTS = [ - "/abc{abc:foo}?", - "/:foo{abc:foo}?", - "{:attr1}?{:attr2/}?", - "{:attr1/}?{:attr2/}?", - "{:foo.}?{:bar.}?", - "{:foo([^\\.]+).}?{:bar.}?", - ":foo(a+):bar(b+)", -]; +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+)", +// ]; for (const path of TESTS) { - try { - const re = pathToRegexp(path, { strict: true }); - const result = checkSync(re.source, re.flags); - if (result.status === "safe") { - console.log("Safe:", path, String(re)); - } else { - console.log("Fail:", path, String(re)); - } - } catch (err) { - try { - const re = pathToRegexp(path); - const result = checkSync(re.source, re.flags); - if (result.status === "safe") { - console.log("Invalid:", path, String(re)); - } else { - console.log("Pass:", path, String(re)); - } - } catch (err) { - console.log("Error:", path, err.message); - } + const { re } = match(path); + const result = checkSync(re.source, re.flags); + if (result.status === "safe") { + safe++; + console.log("Safe:", path, String(re)); + } else { + fail++; + console.log("Fail:", path, String(re)); } } + +console.log("Safe:", safe, "Fail:", fail); diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 23a1814..a837ea4 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -1,5 +1,4 @@ import type { - Path, MatchOptions, Match, ParseOptions, @@ -16,7 +15,7 @@ export interface ParserTestSet { export interface CompileTestSet { path: string; - options?: CompileOptions; + options?: CompileOptions & ParseOptions; tests: Array<{ input: ParamData | undefined; expected: string | null; @@ -24,8 +23,8 @@ export interface CompileTestSet { } export interface MatchTestSet { - path: Path; - options?: MatchOptions; + path: string; + options?: MatchOptions & ParseOptions; tests: Array<{ input: string; matches: (string | undefined)[] | null; @@ -43,7 +42,7 @@ export const PARSER_TESTS: ParserTestSet[] = [ expected: ["/", { name: "test" }], }, { - path: "/:0", + path: '/:"0"', expected: ["/", { name: "0" }], }, { @@ -54,6 +53,14 @@ export const PARSER_TESTS: ParserTestSet[] = [ path: "/:café", expected: ["/", { name: "café" }], }, + { + path: '/:"123"', + expected: ["/", { name: "123" }], + }, + { + path: '/:"1\\"\\2\\"3"', + expected: ["/", { name: '1"2"3' }], + }, ]; export const COMPILE_TESTS: CompileTestSet[] = [ @@ -82,7 +89,7 @@ export const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "/:0", + path: '/:"0"', tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -206,9 +213,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/"], - expected: { path: "/", index: 0, params: {} }, + expected: { path: "/", params: {} }, }, - { input: "/route", matches: null, expected: false }, + { input: "/route", matches: ["/"], expected: false }, ], }, { @@ -217,14 +224,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/route", matches: null, expected: false }, - { input: "/test/route", matches: null, expected: false }, + { input: "/test/route", matches: ["/test"], expected: false }, { input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + matches: ["/test"], + expected: false, }, ], }, @@ -234,14 +241,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/", matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/route", matches: null, expected: false }, { input: "/test", matches: null, expected: false }, { input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, + matches: ["/test/"], + expected: false, }, ], }, @@ -251,47 +258,36 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + matches: ["/route", "route"], + expected: false, }, { input: "/route.json", matches: ["/route.json", "route.json"], expected: { path: "/route.json", - index: 0, params: { test: "route.json" }, }, }, { input: "/route.json/", - matches: ["/route.json/", "route.json"], - expected: { - path: "/route.json/", - index: 0, - params: { test: "route.json" }, - }, + matches: ["/route.json", "route.json"], + expected: false, }, { input: "/route/test", - matches: null, + matches: ["/route", "route"], expected: false, }, - { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, { input: "/caf%C3%A9", matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", - index: 0, params: { test: "café" }, }, }, @@ -300,7 +296,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], expected: { path: "/;,:@&=+$-_.!~*()", - index: 0, params: { test: ";,:@&=+$-_.!~*()" }, }, }, @@ -308,734 +303,102 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, /** - * Case-sensitive paths. - */ - { - path: "/test", - options: { - sensitive: true, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { input: "/TEST", matches: null, expected: false }, - ], - }, - { - path: "/TEST", - options: { - sensitive: true, - }, - tests: [ - { - input: "/TEST", - matches: ["/TEST"], - expected: { path: "/TEST", index: 0, params: {} }, - }, - { input: "/test", matches: null, expected: false }, - ], - }, - - /** - * Non-trailing mode. - */ - { - path: "/test", - options: { - trailing: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: null, - expected: false, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - trailing: false, - }, - tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - ], - }, - { - path: "/:test", - options: { - trailing: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: null, - expected: false, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, - ], - }, - { - path: "/:test/", - options: { - trailing: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "/route//", - matches: ["/route//", "route"], - expected: { path: "/route//", index: 0, params: { test: "route" } }, - }, - ], - }, - - /** - * Non-ending mode. - */ - { - path: "/test", - options: { - end: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test////", - matches: ["/test////"], - expected: { path: "/test////", index: 0, params: {} }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/test/route", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - end: false, - }, - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - end: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route.json", - matches: ["/route.json", "route.json"], - expected: { - path: "/route.json", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "/route.json/", - matches: ["/route.json/", "route.json"], - expected: { - path: "/route.json/", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "/route/test", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route.json/test", - matches: ["/route.json", "route.json"], - expected: { - path: "/route.json", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "///route///test", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, - { - input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], - expected: { - path: "/caf%C3%A9", - index: 0, - params: { test: "café" }, - }, - }, - ], - }, - { - path: "/:test/", - options: { - end: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "/route//test", - matches: null, - expected: false, - }, - ], - }, - { - path: "", - options: { - end: false, - }, - tests: [ - { - input: "", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "route", - matches: null, - expected: false, - }, - { - input: "/route", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/route/", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - ], - }, - - /** - * Non-starting mode. - */ - { - path: "/test", - options: { - start: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/route/test", - matches: ["/test"], - expected: { path: "/test", index: 6, params: {} }, - }, - { - input: "/route/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 6, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - { - input: "/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - start: false, - }, - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 6, params: {} }, - }, - { - input: "/route/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 6, params: {} }, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - start: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: ["/test", "test"], - expected: { path: "/test", index: 6, params: { test: "test" } }, - }, - { - input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, - }, - ], - }, - { - path: "/:test/", - options: { - start: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, - }, - { - input: "/route/test//", - matches: ["/test//", "test"], - expected: { path: "/test//", index: 6, params: { test: "test" } }, - }, - ], - }, - { - path: "", - options: { - start: false, - }, - tests: [ - { - input: "", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "route", - matches: [""], - expected: { path: "", index: 5, params: {} }, - }, - { - input: "/route", - matches: [""], - expected: { path: "", index: 6, params: {} }, - }, - { - input: "/route/", - matches: ["/"], - expected: { path: "/", index: 6, params: {} }, - }, - ], - }, - - /** - * Non-ending and non-trailing modes. - */ - { - path: "/test", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - ], - }, - { - path: "/test/", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - ], - }, - { - path: "/:test/", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, + * Case-sensitive paths. + */ + { + path: "/test", + options: { + sensitive: true, + }, + tests: [ { - input: "/route/test//", - matches: null, - expected: false, + input: "/test", + matches: ["/test"], + expected: { path: "/test", params: {} }, }, + { input: "/TEST", matches: null, expected: false }, + ], + }, + { + path: "/TEST", + options: { + sensitive: true, + }, + tests: [ { - input: "/route//test", - matches: null, - expected: false, + input: "/TEST", + matches: ["/TEST"], + expected: { path: "/TEST", params: {} }, }, + { input: "/test", matches: null, expected: false }, ], }, /** - * Non-starting and non-ending modes. + * Non-ending mode. */ { path: "/test", options: { - start: false, end: false, }, tests: [ { input: "/test", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + matches: ["/test"], + expected: { path: "/test", params: {} }, }, { - input: "/test/route", + input: "/test////", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/route/test", + matches: null, + expected: false, + }, + { + input: "/test/route", matches: ["/test"], - expected: { path: "/test", index: 6, params: {} }, + expected: { path: "/test", params: {} }, + }, + { + input: "/route", + matches: null, + expected: false, }, ], }, { path: "/test/", options: { - start: false, end: false, }, tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, { input: "/test", matches: null, expected: false, }, + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", params: {} }, + }, { input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, + matches: ["/test/"], + expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: null, + matches: ["/test/"], expected: false, }, { @@ -1043,46 +406,66 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: null, expected: false, }, - { - input: "/route/test//deep", - matches: null, - expected: false, - }, ], }, { path: "/:test", options: { - start: false, end: false, }, tests: [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + matches: ["/route", "route"], + expected: { path: "/route", params: { test: "route" } }, + }, + { + input: "/route.json", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, + }, + { + input: "/route.json/", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, }, { input: "/route/test", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/route/test/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + input: "/route.json/test", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, + }, + { + input: "/caf%C3%A9", + matches: ["/caf%C3%A9", "caf%C3%A9"], + expected: { + path: "/caf%C3%A9", + params: { test: "café" }, + }, }, ], }, { path: "/:test/", options: { - start: false, end: false, }, tests: [ @@ -1094,77 +477,80 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route/", matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route/test", - matches: null, + matches: ["/route/", "route"], expected: false, }, { input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, + matches: ["/route/", "route"], + expected: false, }, { - input: "/route/test//", - matches: ["/test//", "test"], - expected: { path: "/test//", index: 6, params: { test: "test" } }, + input: "/route//test", + matches: ["/route/", "route"], + expected: { path: "/route/", params: { test: "route" } }, }, ], }, - - /** - * Optional. - */ { - path: "{/:test}?", + path: "", + options: { + end: false, + }, tests: [ { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + input: "", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, + input: "/", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///route///", - matches: ["///route///", "route"], - expected: { path: "///route///", index: 0, params: { test: "route" } }, + input: "route", + matches: [""], + expected: false, }, { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, + input: "/route", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///", - matches: ["///", undefined], - expected: { path: "///", index: 0, params: {} }, + input: "/route/", + matches: [""], + expected: { path: "", params: {} }, }, ], }, + + /** + * Optional. + */ { path: "{/:test}?", - options: { - trailing: false, - }, tests: [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/route/", - matches: null, + input: "", + matches: ["", undefined], + expected: { path: "", params: {} }, + }, + { + input: "/", + matches: ["", undefined], expected: false, }, - { input: "/", matches: null, expected: false }, - { input: "///", matches: null, expected: false }, ], }, { @@ -1173,22 +559,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/bar", matches: ["/bar", undefined], - expected: { path: "/bar", index: 0, params: {} }, + expected: { path: "/bar", params: {} }, }, { input: "/foo/bar", matches: ["/foo/bar", "foo"], - expected: { path: "/foo/bar", index: 0, params: { test: "foo" } }, - }, - { - input: "///foo///bar", - matches: ["///foo///bar", "foo"], - expected: { path: "///foo///bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo/bar", params: { test: "foo" } }, }, { input: "/foo/bar/", - matches: ["/foo/bar/", "foo"], - expected: { path: "/foo/bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo/bar", "foo"], + expected: false, }, ], }, @@ -1198,17 +579,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "-bar", matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, params: {} }, + expected: { path: "-bar", params: {} }, }, { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar/", "foo"], - expected: { path: "/foo-bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo-bar", "foo"], + expected: false, }, ], }, @@ -1218,17 +599,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/-bar", matches: ["/-bar", undefined], - expected: { path: "/-bar", index: 0, params: {} }, + expected: { path: "/-bar", params: {} }, }, { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar/", "foo"], - expected: { path: "/foo-bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo-bar", "foo"], + expected: false, }, ], }, @@ -1241,34 +622,24 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, + matches: ["", undefined], + expected: false, }, { input: "//", - matches: ["//", undefined], - expected: { path: "//", index: 0, params: {} }, + matches: ["", undefined], + expected: false, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: ["route"] } }, + expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", - index: 0, - params: { test: ["some", "basic", "route"] }, - }, - }, - { - input: "///some///basic///route", - matches: ["///some///basic///route", "some///basic///route"], - expected: { - path: "///some///basic///route", - index: 0, params: { test: ["some", "basic", "route"] }, }, }, @@ -1280,7 +651,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "-bar", matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, params: {} }, + expected: { path: "-bar", params: {} }, }, { input: "/-bar", @@ -1290,14 +661,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", - index: 0, params: { test: ["foo", "baz"] }, }, }, @@ -1323,23 +693,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: ["route"] } }, + expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", - index: 0, - params: { test: ["some", "basic", "route"] }, - }, - }, - { - input: "///some///basic///route", - matches: ["///some///basic///route", "some///basic///route"], - expected: { - path: "///some///basic///route", - index: 0, params: { test: ["some", "basic", "route"] }, }, }, @@ -1361,14 +721,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", - index: 0, params: { test: ["foo", "baz"] }, }, }, @@ -1384,7 +743,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, + expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", @@ -1393,7 +752,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -1419,7 +778,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123-bar", matches: ["/123-bar", "123"], - expected: { path: "/123-bar", index: 0, params: { test: "123" } }, + expected: { path: "/123-bar", params: { test: "123" } }, }, { input: "/123/456-bar", @@ -1434,19 +793,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", ""], - expected: { path: "/", index: 0, params: { test: "" } }, + expected: { path: "/", params: { test: "" } }, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/123", matches: ["/route/123", "route/123"], expected: { path: "/route/123", - index: 0, params: { test: "route/123" }, }, }, @@ -1455,7 +813,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], expected: { path: "/;,:@&=/+$-_.!/~*()", - index: 0, params: { test: ";,:@&=/+$-_.!/~*()" }, }, }, @@ -1467,7 +824,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: "abc" } }, + expected: { path: "/abc", params: { test: "abc" } }, }, { input: "/123", @@ -1476,7 +833,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/123", - matches: null, + matches: ["/abc", "abc"], expected: false, }, ], @@ -1487,12 +844,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/this", matches: ["/this", "this"], - expected: { path: "/this", index: 0, params: { test: "this" } }, + expected: { path: "/this", params: { test: "this" } }, }, { input: "/that", matches: ["/that", "that"], - expected: { path: "/that", index: 0, params: { test: "that" } }, + expected: { path: "/that", params: { test: "that" } }, }, { input: "/foo", @@ -1506,20 +863,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { test: undefined } }, + matches: ["", undefined], + expected: false, }, { input: "/abc", matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: ["abc"] } }, + expected: { path: "/abc", params: { test: ["abc"] } }, }, { input: "/abc/abc", matches: ["/abc/abc", "abc/abc"], expected: { path: "/abc/abc", - index: 0, params: { test: ["abc", "abc"] }, }, }, @@ -1528,7 +884,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/xyz/xyz", "xyz/xyz"], expected: { path: "/xyz/xyz", - index: 0, params: { test: ["xyz", "xyz"] }, }, }, @@ -1537,7 +892,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/abc/xyz", "abc/xyz"], expected: { path: "/abc/xyz", - index: 0, params: { test: ["abc", "xyz"] }, }, }, @@ -1546,13 +900,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/abc/xyz/abc/xyz", "abc/xyz/abc/xyz"], expected: { path: "/abc/xyz/abc/xyz", - index: 0, params: { test: ["abc", "xyz", "abc", "xyz"] }, }, }, { input: "/xyzxyz", - matches: null, + matches: ["/xyz", "xyz"], expected: false, }, ], @@ -1567,7 +920,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "test", matches: ["test"], - expected: { path: "test", index: 0, params: {} }, + expected: { path: "test", params: {} }, }, { input: "/test", @@ -1582,7 +935,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "route", matches: ["route", "route"], - expected: { path: "route", index: 0, params: { test: "route" } }, + expected: { path: "route", params: { test: "route" } }, }, { input: "/route", @@ -1591,8 +944,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "route/", - matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: "route" } }, + matches: ["route", "route"], + expected: false, }, ], }, @@ -1602,12 +955,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "test", matches: ["test", "test"], - expected: { path: "test", index: 0, params: { test: "test" } }, + expected: { path: "test", params: { test: "test" } }, }, { input: "", matches: ["", undefined], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, ], }, @@ -1617,7 +970,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "route/", matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: ["route"] } }, + expected: { path: "route/", params: { test: ["route"] } }, }, { input: "/route", @@ -1634,7 +987,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["foo/bar/", "foo/bar"], expected: { path: "foo/bar/", - index: 0, params: { test: ["foo", "bar"] }, }, }, @@ -1650,7 +1002,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.json", matches: ["/test.json"], - expected: { path: "/test.json", index: 0, params: {} }, + expected: { path: "/test.json", params: {} }, }, { input: "/test", @@ -1670,19 +1022,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.json", matches: ["/test.json", "test"], - expected: { path: "/test.json", index: 0, params: { test: "test" } }, + expected: { path: "/test.json", params: { test: "test" } }, }, { input: "/route.json", matches: ["/route.json", "route"], - expected: { path: "/route.json", index: 0, params: { test: "route" } }, + expected: { path: "/route.json", params: { test: "route" } }, + }, + { + input: "/route.json.json", + matches: ["/route.json", "route"], + expected: false, }, + ], + }, + { + path: "/:test([^/]+).json", + tests: [ { input: "/route.json.json", matches: ["/route.json.json", "route.json"], expected: { path: "/route.json.json", - index: 0, params: { test: "route.json" }, }, }, @@ -1698,7 +1059,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.html", matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, + expected: { path: "/test.html", params: { format: "html" } }, }, { input: "/test", @@ -1715,7 +1076,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html.json", "html", "json"], expected: { path: "/test.html.json", - index: 0, params: { format: "json" }, }, }, @@ -1732,12 +1092,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", matches: ["/test", undefined], - expected: { path: "/test", index: 0, params: { format: undefined } }, + expected: { path: "/test", params: { format: undefined } }, }, { input: "/test.html", matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, + expected: { path: "/test.html", params: { format: "html" } }, }, ], }, @@ -1754,7 +1114,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html", "html"], expected: { path: "/test.html", - index: 0, params: { format: ["html"] }, }, }, @@ -1763,7 +1122,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html.json", "html.json"], expected: { path: "/test.html.json", - index: 0, params: { format: ["html", "json"] }, }, }, @@ -1782,7 +1140,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html", "html"], expected: { path: "/test.html", - index: 0, params: { format: ["html"] }, }, }, @@ -1791,7 +1148,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.hbs.html", "hbs.html"], expected: { path: "/test.hbs.html", - index: 0, params: { format: ["hbs", "html"] }, }, }, @@ -1809,7 +1165,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.html", "route", "html"], expected: { path: "/route.html", - index: 0, params: { test: "route", format: "html" }, }, }, @@ -1823,7 +1178,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.html.json", "route", "html.json"], expected: { path: "/route.html.json", - index: 0, params: { test: "route", format: "html.json" }, }, }, @@ -1835,14 +1189,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route", undefined], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json", matches: ["/route.json", "route", "json"], expected: { path: "/route.json", - index: 0, params: { test: "route", format: "json" }, }, }, @@ -1851,7 +1204,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.json.html", "route", "json.html"], expected: { path: "/route.json.html", - index: 0, params: { test: "route", format: "json.html" }, }, }, @@ -1865,7 +1217,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.htmlz", "route", "html"], expected: { path: "/route.htmlz", - index: 0, params: { test: "route", format: "html" }, }, }, @@ -1886,7 +1237,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, + expected: { path: "/123", params: { "0": "123" } }, }, { input: "/abc", @@ -1895,7 +1246,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -1905,13 +1256,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, + matches: ["", undefined], + expected: false, }, { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, + expected: { path: "/123", params: { "0": "123" } }, }, ], }, @@ -1923,7 +1274,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route(\\123\\)", "123\\"], expected: { path: "/route(\\123\\)", - index: 0, params: { "0": "123\\" }, }, }, @@ -1940,22 +1290,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "", matches: [""], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, { input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, + matches: [""], + expected: false, }, { input: "/foo", - matches: null, + matches: [""], expected: false, }, { input: "/route", matches: ["/route"], - expected: { path: "/route", index: 0, params: {} }, + expected: { path: "/route", params: {} }, }, ], }, @@ -1965,12 +1315,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", ""], - expected: { path: "/", index: 0, params: { "0": "" } }, + expected: { path: "/", params: { "0": "" } }, }, { input: "/login", matches: ["/login", "login"], - expected: { path: "/login", index: 0, params: { "0": "login" } }, + expected: { path: "/login", params: { "0": "login" } }, }, ], }, @@ -1989,7 +1339,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/(testing)", matches: ["/(testing)"], - expected: { path: "/(testing)", index: 0, params: {} }, + expected: { path: "/(testing)", params: {} }, }, ], }, @@ -1999,7 +1349,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/.+*?{}=^!:$[]|", matches: ["/.+*?{}=^!:$[]|"], - expected: { path: "/.+*?{}=^!:$[]|", index: 0, params: {} }, + expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, ], }, @@ -2009,12 +1359,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/u123", matches: ["/test/u123", "u123", undefined], - expected: { path: "/test/u123", index: 0, params: { uid: "u123" } }, + expected: { path: "/test/u123", params: { uid: "u123" } }, }, { input: "/test/c123", matches: ["/test/c123", undefined, "c123"], - expected: { path: "/test/c123", index: 0, params: { cid: "c123" } }, + expected: { path: "/test/c123", params: { cid: "c123" } }, }, ], }, @@ -2028,14 +1378,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/icon-240.png", matches: ["/icon-240.png", "240"], - expected: { path: "/icon-240.png", index: 0, params: { res: "240" } }, + expected: { path: "/icon-240.png", params: { res: "240" } }, }, { input: "/apple-icon-240.png", matches: ["/apple-icon-240.png", "240"], expected: { path: "/apple-icon-240.png", - index: 0, params: { res: "240" }, }, }, @@ -2053,7 +1402,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/match/route", "match", "route"], expected: { path: "/match/route", - index: 0, params: { foo: "match", bar: "route" }, }, }, @@ -2065,7 +1413,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo(test)/bar", matches: ["/foo(test)/bar", "foo"], - expected: { path: "/foo(test)/bar", index: 0, params: { foo: "foo" } }, + expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { input: "/foo/bar", @@ -2082,7 +1430,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/endpoint/user", "endpoint", "user"], expected: { path: "/endpoint/user", - index: 0, params: { remote: "endpoint", user: "user" }, }, }, @@ -2091,7 +1438,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/endpoint/user-name", "endpoint", "user-name"], expected: { path: "/endpoint/user-name", - index: 0, params: { remote: "endpoint", user: "user-name" }, }, }, @@ -2100,7 +1446,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/foo.bar/user-name", "foo.bar", "user-name"], expected: { path: "/foo.bar/user-name", - index: 0, params: { remote: "foo.bar", user: "user-name" }, }, }, @@ -2112,7 +1457,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route?", matches: ["/route?", "route"], - expected: { path: "/route?", index: 0, params: { foo: "route" } }, + expected: { path: "/route?", params: { foo: "route" } }, }, { input: "/route", @@ -2127,7 +1472,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foobar", matches: ["/foobar", "foo"], - expected: { path: "/foobar", index: 0, params: { foo: ["foo"] } }, + expected: { path: "/foobar", params: { foo: ["foo"] } }, }, { input: "/foo/bar", @@ -2136,12 +1481,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/barbar", - matches: ["/foo/barbar", "foo/bar"], - expected: { - path: "/foo/barbar", - index: 0, - params: { foo: ["foo", "bar"] }, - }, + matches: null, + expected: false, }, ], }, @@ -2151,12 +1492,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foobaz", matches: ["/foobaz", "foo"], - expected: { path: "/foobaz", index: 0, params: { pre: "foo" } }, + expected: { path: "/foobaz", params: { pre: "foo" } }, }, { input: "/baz", matches: ["/baz", undefined], - expected: { path: "/baz", index: 0, params: { pre: undefined } }, + expected: { path: "/baz", params: { pre: undefined } }, }, ], }, @@ -2168,7 +1509,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -2187,7 +1527,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -2196,7 +1535,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello()", "hello", undefined], expected: { path: "/hello()", - index: 0, params: { foo: "hello", bar: undefined }, }, }, @@ -2208,20 +1546,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/video", matches: ["/video", "video", undefined], - expected: { path: "/video", index: 0, params: { postType: "video" } }, + expected: { path: "/video", params: { postType: "video" } }, }, { input: "/video+test", matches: ["/video+test", "video", "+test"], expected: { path: "/video+test", - index: 0, params: { 0: "+test", postType: "video" }, }, }, { input: "/video+", - matches: null, + matches: ["/video", "video", undefined], expected: false, }, ], @@ -2239,21 +1576,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["-ext", undefined, undefined], expected: { path: "-ext", - index: 0, params: { foo: undefined, bar: undefined }, }, }, { input: "/foo-ext", matches: ["/foo-ext", "foo", undefined], - expected: { path: "/foo-ext", index: 0, params: { foo: "foo" } }, + expected: { path: "/foo-ext", params: { foo: "foo" } }, }, { input: "/foo/bar-ext", matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", - index: 0, params: { foo: "foo", bar: "bar" }, }, }, @@ -2270,14 +1605,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-ext", matches: ["/foo-ext", "foo", undefined], - expected: { path: "/foo-ext", index: 0, params: { required: "foo" } }, + expected: { path: "/foo-ext", params: { required: "foo" } }, }, { input: "/foo/bar-ext", matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", - index: 0, params: { required: "foo", optional: "bar" }, }, }, @@ -2298,7 +1632,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/café", matches: ["/café", "café"], - expected: { path: "/café", index: 0, params: { foo: "café" } }, + expected: { path: "/café", params: { foo: "café" } }, }, ], }, @@ -2313,7 +1647,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", - index: 0, params: { foo: "caf%C3%A9" }, }, }, @@ -2325,7 +1658,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/café", matches: ["/café"], - expected: { path: "/café", index: 0, params: {} }, + expected: { path: "/café", params: {} }, }, ], }, @@ -2338,7 +1671,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/caf%C3%A9", matches: ["/caf%C3%A9"], - expected: { path: "/caf%C3%A9", index: 0, params: {} }, + expected: { path: "/caf%C3%A9", params: {} }, }, ], }, @@ -2357,7 +1690,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["example.com", "example"], expected: { path: "example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2366,7 +1698,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["github.com", "github"], expected: { path: "github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2383,7 +1714,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2392,7 +1722,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2407,14 +1736,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "mail.com", matches: ["mail.com", undefined], - expected: { path: "mail.com", index: 0, params: { domain: undefined } }, + expected: { path: "mail.com", params: { domain: undefined } }, }, { input: "mail.example.com", matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2423,7 +1751,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2438,12 +1765,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "example.com", matches: ["example.com", "com"], - expected: { path: "example.com", index: 0, params: { ext: "com" } }, + expected: { path: "example.com", params: { ext: "com" } }, }, { input: "example.org", matches: ["example.org", "org"], - expected: { path: "example.org", index: 0, params: { ext: "org" } }, + expected: { path: "example.org", params: { ext: "org" } }, }, ], }, @@ -2457,11 +1784,11 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "this is a test", matches: ["this is"], - expected: { path: "this is", index: 0, params: {} }, + expected: { path: "this is", params: {} }, }, { input: "this isn't", - matches: null, + matches: ["this is"], expected: false, }, ], @@ -2476,12 +1803,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "$x", matches: ["$x", "x", undefined], - expected: { path: "$x", index: 0, params: { foo: "x" } }, + expected: { path: "$x", params: { foo: "x" } }, }, { input: "$x$y", matches: ["$x$y", "x", "y"], - expected: { path: "$x$y", index: 0, params: { foo: "x", bar: "y" } }, + expected: { path: "$x$y", params: { foo: "x", bar: "y" } }, }, ], }, @@ -2491,12 +1818,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "$x", matches: ["$x", "x"], - expected: { path: "$x", index: 0, params: { foo: ["x"] } }, + expected: { path: "$x", params: { foo: ["x"] } }, }, { input: "$x$y", matches: ["$x$y", "x$y"], - expected: { path: "$x$y", index: 0, params: { foo: ["x", "y"] } }, + expected: { path: "$x$y", params: { foo: ["x", "y"] } }, }, ], }, @@ -2506,14 +1833,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "name", matches: ["name", undefined, undefined, undefined], - expected: { path: "name", index: 0, params: {} }, + expected: { path: "name", params: {} }, }, { input: "name/test", matches: ["name/test", "test", undefined, undefined], expected: { path: "name/test", - index: 0, params: { attr1: "test" }, }, }, @@ -2522,7 +1848,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1", "1", undefined, undefined], expected: { path: "name/1", - index: 0, params: { attr1: "1" }, }, }, @@ -2531,7 +1856,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2", "1", "2", undefined], expected: { path: "name/1-2", - index: 0, params: { attr1: "1", attr2: "2" }, }, }, @@ -2540,18 +1864,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2-3", "1", "2", "3"], expected: { path: "name/1-2-3", - index: 0, params: { attr1: "1", attr2: "2", attr3: "3" }, }, }, { input: "name/foo-bar/route", - matches: null, + matches: ["name/foo-bar", "foo", "bar", undefined], expected: false, }, { input: "name/test/route", - matches: null, + matches: ["name/test", "test", undefined, undefined], expected: false, }, ], @@ -2562,14 +1885,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "name", matches: ["name", undefined], - expected: { path: "name", index: 0, params: {} }, + expected: { path: "name", params: {} }, }, { input: "name/1", matches: ["name/1", "1"], expected: { path: "name/1", - index: 0, params: { attrs: ["1"] }, }, }, @@ -2578,7 +1900,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2", "1-2"], expected: { path: "name/1-2", - index: 0, params: { attrs: ["1", "2"] }, }, }, @@ -2587,18 +1908,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2-3", "1-2-3"], expected: { path: "name/1-2-3", - index: 0, params: { attrs: ["1", "2", "3"] }, }, }, { input: "name/foo-bar/route", - matches: null, + matches: ["name/foo-bar", "foo-bar"], expected: false, }, { input: "name/test/route", - matches: null, + matches: ["name/test", "test"], expected: false, }, ], @@ -2613,7 +1933,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, + expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", @@ -2622,17 +1942,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, { input: "/123.123", matches: ["/123.123", "123.123"], - expected: { path: "/123.123", index: 0, params: { test: "123.123" } }, + expected: { path: "/123.123", params: { test: "123.123" } }, }, { input: "/123.abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -2643,7 +1963,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/login", @@ -2662,14 +1982,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/user/123", matches: ["/user/123", undefined, "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, + expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", matches: ["/users/123", "s", "123"], expected: { path: "/users/123", - index: 0, params: { 0: "s", user: "123" }, }, }, @@ -2681,12 +2000,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/user/123", matches: ["/user/123", "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, + expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", matches: ["/users/123", "123"], - expected: { path: "/users/123", index: 0, params: { user: "123" } }, + expected: { path: "/users/123", params: { user: "123" } }, }, ], }, @@ -2702,7 +2021,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", - index: 0, params: { path: ["hello", "world"], ext: ["txt"] }, }, }, @@ -2711,18 +2029,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt.png", "hello/world", "txt.png"], expected: { path: "/files/hello/world.txt.png", - index: 0, params: { path: ["hello", "world"], ext: ["txt", "png"] }, }, }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg/gif", "my/photo.jpg/gif", undefined], - expected: { - path: "/files/my/photo.jpg/gif", - index: 0, - params: { path: ["my", "photo.jpg", "gif"], ext: undefined }, - }, + matches: ["/files/my/photo.jpg", "my/photo", "jpg"], + expected: false, }, ], }, @@ -2734,18 +2047,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", - index: 0, params: { path: ["hello", "world"], ext: "txt" }, }, }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg/gif", "my/photo.jpg/gif", undefined], - expected: { - path: "/files/my/photo.jpg/gif", - index: 0, - params: { path: ["my", "photo.jpg", "gif"], ext: undefined }, - }, + matches: ["/files/my/photo.jpg", "my/photo", "jpg"], + expected: false, }, ], }, @@ -2755,7 +2063,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "#/", matches: ["#/", undefined], - expected: { path: "#/", index: 0, params: {} }, + expected: { path: "#/", params: {} }, }, ], }, @@ -2763,11 +2071,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ path: "/foo{/:bar}*", tests: [ { - input: "/foo/test1//test2", - matches: ["/foo/test1//test2", "test1//test2"], + input: "/foo/test1/test2", + matches: ["/foo/test1/test2", "test1/test2"], expected: { - path: "/foo/test1//test2", - index: 0, + path: "/foo/test1/test2", params: { bar: ["test1", "test2"] }, }, }, @@ -2784,7 +2091,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/entity/foo/", matches: ["/entity/foo/", "foo", undefined], - expected: { path: "/entity/foo/", index: 0, params: { id: "foo" } }, + expected: { path: "/entity/foo/", params: { id: "foo" } }, }, ], }, @@ -2799,19 +2106,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/", matches: ["/test/", undefined], - expected: { path: "/test/", index: 0, params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/test/route", matches: ["/test/route", "route"], - expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, + expected: { path: "/test/route", params: { "0": ["route"] } }, }, { input: "/test/route/nested", matches: ["/test/route/nested", "route/nested"], expected: { path: "/test/route/nested", - index: 0, params: { "0": ["route", "nested"] }, }, }, @@ -2827,19 +2133,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, + expected: { path: "/", params: { "0": undefined } }, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + expected: { path: "/route", params: { "0": ["route"] } }, }, { input: "/route/nested", matches: ["/route/nested", "route/nested"], expected: { path: "/route/nested", - index: 0, params: { "0": ["route", "nested"] }, }, }, @@ -2851,12 +2156,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "/"], - expected: { path: "/", index: 0, params: { "0": ["", ""] } }, + expected: { path: "/", params: { "0": ["", ""] } }, }, { input: "/test", matches: ["/test", "/test"], - expected: { path: "/test", index: 0, params: { "0": ["", "test"] } }, + expected: { path: "/test", params: { "0": ["", "test"] } }, }, ], }, @@ -2867,32 +2172,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "/"], - expected: { path: "/", index: 0, params: { "0": "/" } }, + expected: { path: "/", params: { "0": "/" } }, }, { input: "/test", matches: ["/test", "/test"], - expected: { path: "/test", index: 0, params: { "0": "/test" } }, - }, - ], - }, - - /** - * No loose. - */ - { - path: "/test", - options: { loose: false }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "//test", - matches: null, - expected: false, + expected: { path: "/test", params: { "0": "/test" } }, }, ], }, @@ -2906,14 +2191,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route", undefined], - expected: { path: "/route", index: 0, params: { foo: "route" } }, + expected: { path: "/route", params: { foo: "route" } }, }, { input: "/route/test/again", matches: ["/route/test/again", "route", "again"], expected: { path: "/route/test/again", - index: 0, params: { foo: "route", bar: "again" }, }, }, @@ -2929,14 +2213,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "test"], - expected: { path: "/", index: 0, params: { foo: ["test"] } }, + expected: { path: "/", params: { foo: ["test"] } }, }, { input: "/", matches: ["/", "test>", - index: 0, params: { foo: ["test", "again"] }, }, }, @@ -2952,21 +2235,20 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "", matches: ["", undefined, undefined], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, { input: "test/", matches: ["test/", "test", undefined], expected: { path: "test/", - index: 0, params: { foo: "test" }, }, }, { input: "a/b.", matches: ["a/b.", "a", "b"], - expected: { path: "a/b.", index: 0, params: { foo: "a", bar: "b" } }, + expected: { path: "a/b.", params: { foo: "a", bar: "b" } }, }, ], }, @@ -2976,31 +2258,30 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", undefined], - expected: { path: "/abc", index: 0, params: {} }, + expected: { path: "/abc", params: {} }, }, { input: "/abcabc", - matches: null, + matches: ["/abc", undefined], expected: false, }, { input: "/abcabc123", matches: ["/abcabc123", "123"], - expected: { path: "/abcabc123", index: 0, params: { foo: "123" } }, + expected: { path: "/abcabc123", params: { foo: "123" } }, }, { input: "/abcabcabc123", matches: ["/abcabcabc123", "abc123"], expected: { path: "/abcabcabc123", - index: 0, params: { foo: "abc123" }, }, }, { input: "/abcabcabc", matches: ["/abcabcabc", "abc"], - expected: { path: "/abcabcabc", index: 0, params: { foo: "abc" } }, + expected: { path: "/abcabcabc", params: { foo: "abc" } }, }, ], }, @@ -3009,39 +2290,33 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc", undefined], - expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + matches: null, + expected: false, }, { input: "/abcabc", - matches: ["/abcabc", "abcabc", undefined], - expected: { path: "/abcabc", index: 0, params: { foo: "abcabc" } }, + matches: null, + expected: false, }, { input: "/abcabc123", - matches: ["/abcabc123", "abc", "123"], - expected: { - path: "/abcabc123", - index: 0, - params: { foo: "abc", bar: "123" }, - }, + matches: null, + expected: false, }, { - input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc", "abc123"], + input: "/acb", + matches: ["/acb", "acb", undefined], expected: { - path: "/abcabcabc123", - index: 0, - params: { foo: "abc", bar: "abc123" }, + path: "/acb", + params: { foo: "acb" }, }, }, { - input: "/abcabcabc", - matches: ["/abcabcabc", "abc", "abc"], + input: "/acbabc123", + matches: ["/acbabc123", "acb", "123"], expected: { - path: "/abcabcabc", - index: 0, - params: { foo: "abc", bar: "abc" }, + path: "/acbabc123", + params: { foo: "acb", bar: "123" }, }, }, ], @@ -3061,30 +2336,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabc123", - matches: ["/abcabc123", "abc", "123"], - expected: { - path: "/abcabc123", - index: 0, - params: { foo: "abc", bar: "123" }, - }, - }, - { - input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc", "abc123"], - expected: { - path: "/abcabcabc123", - index: 0, - params: { foo: "abc", bar: "abc123" }, - }, - }, - { - input: "/abcabcabc", - matches: ["/abcabcabc", "abc", "abc"], - expected: { - path: "/abcabcabc", - index: 0, - params: { foo: "abc", bar: "abc" }, - }, + matches: null, + expected: false, }, ], }, @@ -3094,12 +2347,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", "abc", undefined], - expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + expected: { path: "/abc", params: { foo: "abc" } }, }, { input: "/abc.txt", matches: ["/abc.txt", "abc.txt", undefined], - expected: { path: "/abc.txt", index: 0, params: { foo: "abc.txt" } }, + expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, }, ], }, @@ -3111,7 +2364,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route|world|", "world"], expected: { path: "/route|world|", - index: 0, params: { param: "world" }, }, }, @@ -3130,7 +2382,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello|world|", "hello", "world"], expected: { path: "/hello|world|", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -3147,7 +2398,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "x@y", matches: ["x@y", "x", "y"], - expected: { path: "x@y", index: 0, params: { foo: "x", bar: "y" } }, + expected: { path: "x@y", params: { foo: "x", bar: "y" } }, }, { input: "x@", @@ -3169,14 +2420,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "%25hello", matches: ["%25hello", "hello", undefined], - expected: { path: "%25hello", index: 0, params: { foo: "hello" } }, + expected: { path: "%25hello", params: { foo: "hello" } }, }, { input: "%25hello%25world", matches: ["%25hello%25world", "hello", "world"], expected: { path: "%25hello%25world", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -3185,7 +2435,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["%25555%25222", "555", "222"], expected: { path: "%25555%25222", - index: 0, params: { foo: "555", bar: "222" }, }, }, diff --git a/src/index.spec.ts b/src/index.spec.ts index d0dd420..73f2c78 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { pathToRegexp, parse, compile, match } from "./index.js"; +import { parse, compile, match } from "./index.js"; import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; /** @@ -7,60 +7,48 @@ import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; */ describe("path-to-regexp", () => { describe("arguments", () => { - it("should accept an array of keys as the second argument", () => { - const re = pathToRegexp("/user/:id", { end: false }); - - const expectedKeys = [ - { - name: "id", - pattern: undefined, - }, - ]; - - expect(re.keys).toEqual(expectedKeys); - expect(exec(re, "/user/123/show")).toEqual(["/user/123", "123"]); - }); - - it("should accept parse result as input", () => { - const tokens = parse("/user/:id"); - const re = pathToRegexp(tokens); - expect(exec(re, "/user/123")).toEqual(["/user/123", "123"]); - }); - it("should throw on non-capturing pattern", () => { - expect(() => { - pathToRegexp("/:foo(?:\\d+(\\.\\d+)?)"); - }).toThrow(new TypeError('Pattern cannot start with "?" at 6')); + expect(() => match("/:foo(?:\\d+(\\.\\d+)?)")).toThrow( + new TypeError( + 'Pattern cannot start with "?" at 6: https://git.new/pathToRegexpError', + ), + ); }); it("should throw on nested capturing group", () => { - expect(() => { - pathToRegexp("/:foo(\\d+(\\.\\d+)?)"); - }).toThrow(new TypeError("Capturing groups are not allowed at 9")); + expect(() => match("/:foo(\\d+(\\.\\d+)?)")).toThrow( + new TypeError( + "Capturing groups are not allowed at 9: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on unbalanced pattern", () => { - expect(() => { - pathToRegexp("/:foo(abc"); - }).toThrow(new TypeError("Unbalanced pattern at 5")); + expect(() => match("/:foo(abc")).toThrow( + new TypeError( + "Unbalanced pattern at 5: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on missing pattern", () => { - expect(() => { - pathToRegexp("/:foo()"); - }).toThrow(new TypeError("Missing pattern at 5")); + expect(() => match("/:foo()")).toThrow( + new TypeError( + "Missing pattern at 5: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on missing name", () => { - expect(() => { - pathToRegexp("/:(test)"); - }).toThrow(new TypeError("Missing parameter name at 2")); + expect(() => match("/:(test)")).toThrow( + new TypeError( + "Missing parameter name at 2: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on nested groups", () => { - expect(() => { - pathToRegexp("/{a{b:foo}}"); - }).toThrow( + expect(() => match("/{a{b:foo}}")).toThrow( new TypeError( "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", ), @@ -68,14 +56,28 @@ describe("path-to-regexp", () => { }); it("should throw on repeat parameters without a separator", () => { - expect(() => { - pathToRegexp("{:x}*"); - }).toThrow( + expect(() => match("{:x}*")).toThrow( new TypeError( `Missing separator for "x": https://git.new/pathToRegexpError`, ), ); }); + + it("should throw on unterminated quote", () => { + expect(() => match('/:"foo')).toThrow( + new TypeError( + "Unterminated quote at 2: https://git.new/pathToRegexpError", + ), + ); + }); + + it("should throw on invalid *", () => { + expect(() => match("/:foo*")).toThrow( + new TypeError( + "Unexpected * at 5, you probably want `/*` or `{/:foo}*`: https://git.new/pathToRegexpError", + ), + ); + }); }); describe.each(PARSER_TESTS)( @@ -107,10 +109,9 @@ describe("path-to-regexp", () => { "match $path with $options", ({ path, options, tests }) => { it.each(tests)("should match $input", ({ input, matches, expected }) => { - const re = pathToRegexp(path, options); const fn = match(path, options); - expect(exec(re, input)).toEqual(matches); + expect(exec(fn.re, input)).toEqual(matches); expect(fn(input)).toEqual(expected); }); }, diff --git a/src/index.ts b/src/index.ts index 2dd3d6f..ec4fad1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,53 +25,25 @@ export interface ParseOptions { encodePath?: Encode; } -export interface PathToRegexpOptions extends ParseOptions { +export interface PathOptions { /** * Regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; - /** - * Allow the delimiter to be arbitrarily repeated. (default: `true`) - */ - loose?: boolean; - /** - * Verify patterns are valid and safe to use. (default: `false`) - */ - strict?: boolean; - /** - * Match from the beginning of the string. (default: `true`) - */ - start?: boolean; - /** - * Match to the end of the string. (default: `true`) - */ - end?: boolean; - /** - * Allow optional trailing delimiter to match. (default: `true`) - */ - trailing?: boolean; } -export interface MatchOptions extends PathToRegexpOptions { +export interface MatchOptions extends PathOptions { /** * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) */ decode?: Decode | false; -} - -export interface CompileOptions extends ParseOptions { - /** - * Regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * Allow the delimiter to be arbitrarily repeated. (default: `true`) - */ - loose?: boolean; /** - * Verify patterns are valid and safe to use. (default: `false`) + * Matches the path completely without trailing characters. (default: `true`) */ - strict?: boolean; + end?: boolean; +} + +export interface CompileOptions extends PathOptions { /** * Verifies the function is producing a valid path. (default: `true`) */ @@ -321,7 +293,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - pattern: `(?:(?!${escape(delimiter)}).)*`, + pattern: `${negate(escape(delimiter))}*`, modifier: "*", separator: delimiter, }); @@ -362,33 +334,40 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { * Compile a string to a template function for the path. */ export function compile

( - path: Path, - options: CompileOptions = {}, + path: string, + options: CompileOptions & ParseOptions = {}, ) { - const data = path instanceof TokenData ? path : parse(path, options); - return compileTokens

(data, options); + return $compile

(parse(path, options), options); } export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; +/** + * Check if a key repeats. + */ +export function isRepeat(key: Key) { + return key.modifier === "+" || key.modifier === "*"; +} + +/** + * Check if a key is optional. + */ +export function isOptional(key: Key) { + return key.modifier === "?" || key.modifier === "*"; +} + /** * Convert a single token into a path building function. */ -function tokenToFunction( - token: Token, +function keyToFunction( + token: Key, encode: Encode | false, ): (data: ParamData) => string { - if (typeof token === "string") { - return () => token; - } - const encodeValue = encode || NOOP_VALUE; - const repeated = token.modifier === "+" || token.modifier === "*"; - const optional = token.modifier === "?" || token.modifier === "*"; const { prefix = "", suffix = "", separator = suffix + prefix } = token; - if (encode && repeated) { + if (encode && isRepeat(token)) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}/${index}" to be a string`); @@ -406,7 +385,7 @@ function tokenToFunction( return prefix + value.map(stringify).join(separator) + suffix; }; - if (optional) { + if (isOptional(token)) { return (data): string => { const value = data[token.name]; if (value == null) return ""; @@ -427,7 +406,7 @@ function tokenToFunction( return prefix + encodeValue(value) + suffix; }; - if (optional) { + if (isOptional(token)) { return (data): string => { const value = data[token.name]; if (value == null) return ""; @@ -444,25 +423,21 @@ function tokenToFunction( /** * Transform tokens into a path building function. */ -function compileTokens

( +export function $compile

( data: TokenData, options: CompileOptions, ): PathFunction

{ - const { - encode = encodeURIComponent, - loose = true, - validate = true, - strict = false, - } = options; + const { encode = encodeURIComponent, validate = true } = options; const flags = toFlags(options); - const stringify = toStringify(loose, data.delimiter); - const sources = toRegExpSource(data, stringify, [], flags, strict); + const sources = toRegExpSource(data, []); // Compile all the tokens into regexps. const encoders: Array<(data: ParamData) => string> = data.tokens.map( (token, index) => { - const fn = tokenToFunction(token, encode); - if (!validate || typeof token === "string") return fn; + if (typeof token === "string") return () => token; + + const fn = keyToFunction(token, encode); + if (!validate) return fn; const validRe = new RegExp(`^${sources[index]}$`, flags); @@ -490,7 +465,6 @@ function compileTokens

( */ export interface MatchResult

{ path: string; - index: number; params: P; } @@ -502,80 +476,83 @@ export type Match

= false | MatchResult

; /** * The match function takes a string and returns whether it matched the path. */ -export type MatchFunction

= (path: string) => Match

; +export type MatchFunction

= (( + path: string, +) => Match

) & { re: RegExp }; + +const isEnd = (input: string, match: string) => input.length === match.length; +const isDelimiterOrEnd = + (delimiter: string) => (input: string, match: string) => { + return ( + match.length === input.length || + input.slice(match.length, match.length + delimiter.length) === delimiter + ); + }; /** * Create path match function from `path-to-regexp` spec. */ -export function match

( - path: Path, +export function $match

( + data: TokenData, options: MatchOptions = {}, ): MatchFunction

{ - const { decode = decodeURIComponent, loose = true } = options; - const data = path instanceof TokenData ? path : parse(path, options); - const stringify = toStringify(loose, data.delimiter); - const keys: Key[] = []; - const re = tokensToRegexp(data, keys, options); + const { decode = decodeURIComponent, end = true } = options; + const re = tokensToRegexp(data, options); - const decoders = keys.map((key) => { + const decoders = re.keys.map((key) => { if (decode && (key.modifier === "+" || key.modifier === "*")) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; - const re = new RegExp(stringify(separator), "g"); + const re = new RegExp(escape(separator), "g"); return (value: string) => value.split(re).map(decode); } return decode || NOOP_VALUE; }); - return function match(input: string) { - const m = re.exec(input); - if (!m) return false; + const validate = end ? isEnd : isDelimiterOrEnd(data.delimiter); - const { 0: path, index } = m; - const params = Object.create(null); + return Object.assign( + function match(input: string) { + const m = re.exec(input); + if (!m) return false; - for (let i = 1; i < m.length; i++) { - if (m[i] === undefined) continue; + const { 0: path } = m; + if (!validate(input, path)) return false; + const params = Object.create(null); - const key = keys[i - 1]; - const decoder = decoders[i - 1]; - params[key.name] = decoder(m[i]); - } + for (let i = 1; i < m.length; i++) { + if (m[i] === undefined) continue; - return { path, index, params }; - }; -} + const key = re.keys[i - 1]; + const decoder = decoders[i - 1]; + params[key.name] = decoder(m[i]); + } -/** - * Escape a regular expression string. - */ -function escape(str: string) { - return str.replace(/([.+*?^${}()[\]|/\\])/g, "\\$1"); + return { path, params }; + }, + { re }, + ); } -/** - * Escape and repeat loose characters for regular expressions. - */ -function looseReplacer(value: string, loose: string) { - const escaped = escape(value); - return loose ? `(?:${escaped})+(?!${escaped})` : escaped; +export function match

( + path: string, + options: MatchOptions & ParseOptions = {}, +): MatchFunction

{ + return $match(parse(path, options), options); } /** - * Encode all non-delimiter characters using the encode function. + * Escape a regular expression string. */ -function toStringify(loose: boolean, delimiter: string) { - if (!loose) return escape; - - const re = new RegExp(`(?:(?!${escape(delimiter)}).)+|(.)`, "g"); - return (value: string) => value.replace(re, looseReplacer); +function escape(str: string) { + return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); } /** * Get the flags for a regexp from the options. */ function toFlags(options: { sensitive?: boolean }) { - return options.sensitive ? "" : "i"; + return options.sensitive ? "s" : "is"; } /** @@ -598,47 +575,30 @@ export type Token = string | Key; /** * Expose a function for taking tokens and returning a RegExp. */ -function tokensToRegexp( - data: TokenData, - keys: Key[], - options: PathToRegexpOptions, -): RegExp { - const { - trailing = true, - loose = true, - start = true, - end = true, - strict = false, - } = options; +function tokensToRegexp(data: TokenData, options: PathOptions) { const flags = toFlags(options); - const stringify = toStringify(loose, data.delimiter); - const sources = toRegExpSource(data, stringify, keys, flags, strict); - let pattern = start ? "^" : ""; - pattern += sources.join(""); - if (trailing) pattern += `(?:${stringify(data.delimiter)})?`; - pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; - - return new RegExp(pattern, flags); + const keys: Key[] = []; + const sources = toRegExpSource(data, keys); + const regexp = new RegExp(`^${sources.join("")}`, flags); + return Object.assign(regexp, { keys }); } /** * Convert a token into a regexp string (re-used for path validation). */ -function toRegExpSource( - data: TokenData, - stringify: Encode, - keys: Key[], - flags: string, - strict: boolean, -): string[] { - const defaultPattern = `(?:(?!${escape(data.delimiter)}).)+?`; +function toRegExpSource(data: TokenData, keys: Key[]): string[] { + const delim = escape(data.delimiter); + const sources = Array(data.tokens.length); let backtrack = ""; - let safe = true; - return data.tokens.map((token, index) => { + let i = data.tokens.length; + + while (i--) { + const token = data.tokens[i]; + if (typeof token === "string") { - backtrack = token; - return stringify(token); + sources[i] = backtrack = escape(token); + continue; } const { @@ -648,27 +608,17 @@ function toRegExpSource( modifier = "", } = token; - const pre = stringify(prefix); - const post = stringify(suffix); + const pre = escape(prefix); + const post = escape(suffix); if (token.name) { - const pattern = token.pattern ? `(?:${token.pattern})` : defaultPattern; - const re = checkPattern(pattern, token.name, flags); + let pattern = token.pattern || ""; - safe ||= safePattern(re, prefix || backtrack); - if (!safe) { - throw new TypeError( - `Ambiguous pattern for "${token.name}": ${DEBUG_URL}`, - ); - } - safe = !strict || safePattern(re, suffix); - backtrack = ""; + keys.unshift(token); - keys.push(token); - - if (modifier === "+" || modifier === "*") { + if (isRepeat(token)) { const mod = modifier === "*" ? "?" : ""; - const sep = stringify(separator); + const sep = escape(separator); if (!sep) { throw new TypeError( @@ -676,51 +626,25 @@ function toRegExpSource( ); } - safe ||= !strict || safePattern(re, separator); - if (!safe) { - throw new TypeError( - `Ambiguous pattern for "${token.name}" separator: ${DEBUG_URL}`, - ); - } - safe = !strict; - - return `(?:${pre}(${pattern}(?:${sep}${pattern})*)${post})${mod}`; + pattern ||= `${negate(delim, sep, post || backtrack)}+`; + sources[i] = + `(?:${pre}((?:${pattern})(?:${sep}(?:${pattern}))*)${post})${mod}`; + } else { + pattern ||= `${negate(delim, post || backtrack)}+`; + sources[i] = `(?:${pre}(${pattern})${post})${modifier}`; } - return `(?:${pre}(${pattern})${post})${modifier}`; + backtrack = pre || pattern; + } else { + sources[i] = `(?:${pre}${post})${modifier}`; + backtrack = `${pre}${post}`; } - - return `(?:${pre}${post})${modifier}`; - }); -} - -function checkPattern(pattern: string, name: string, flags: string) { - try { - return new RegExp(`^${pattern}$`, flags); - } catch (err: any) { - throw new TypeError(`Invalid pattern for "${name}": ${err.message}`); } -} -function safePattern(re: RegExp, value: string) { - return value ? !re.test(value) : false; + return sources; } -/** - * Repeated and simple input types. - */ -export type Path = string | TokenData; - -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - */ -export function pathToRegexp(path: Path, options: PathToRegexpOptions = {}) { - const data = path instanceof TokenData ? path : parse(path, options); - const keys: Key[] = []; - const regexp = tokensToRegexp(data, keys, options); - return Object.assign(regexp, { keys }); +function negate(...args: string[]) { + const values = Array.from(new Set(args)).filter(Boolean); + return `(?:(?!${values.join("|")}).)`; }