From 6e12cf54770699be5f83fd9b2cd43d2f71b1e01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Marqu=C3=ADnez=20Prado?= <25435858+inigomarquinez@users.noreply.github.com> Date: Mon, 8 Apr 2024 23:27:11 +0200 Subject: [PATCH 01/55] Add OSSF scorecard (#302) --- .github/workflows/scorecard.yml | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..0e064f4 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security + +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2 + with: + sarif_file: results.sarif From 80a1eb6c5dd32e3f61f4f6fcbce74ef2adaa9a69 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 8 Apr 2024 14:31:26 -0700 Subject: [PATCH 02/55] Adjust CI permissions --- .github/workflows/ci.yml | 3 ++- .github/workflows/scorecard.yml | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35714de..23c96d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,8 @@ name: CI on: - push - pull_request +permissions: + contents: read jobs: test: name: Node.js ${{ matrix.node-version }} @@ -22,7 +24,6 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - run: npm install -g npm@8 - run: npm ci - run: npm test - uses: codecov/codecov-action@v1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0e064f4..5fcb48c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -11,12 +11,13 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '16 21 * * 1' + - cron: "16 21 * * 1" push: - branches: [ "master" ] + branches: ["master"] # Declare default permissions as read only. -permissions: read-all +permissions: + contents: read jobs: analysis: From b893429f2bf0dabf1a38d7adc77f116b37f6cb6d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 14 May 2024 14:23:04 -0700 Subject: [PATCH 03/55] Code cleanup and consistency with optional empty strings --- src/index.spec.ts | 16 ++++++++-------- src/index.ts | 40 ++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index f681e87..8e041cf 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1215,7 +1215,7 @@ const TESTS: Test[] = [ [ ["route", ["route", "route"]], ["/route", null], - ["", ["", undefined]], + ["", ["", ""]], ["route/foobar", null], ], [ @@ -1888,9 +1888,9 @@ const TESTS: Test[] = [ ], [ ["/test", null], - ["/test/", ["/test/", undefined, undefined]], - ["/test/u123", ["/test/u123", "u123", undefined]], - ["/test/c123", ["/test/c123", undefined, "c123"]], + ["/test/", ["/test/", "", ""]], + ["/test/u123", ["/test/u123", "u123", ""]], + ["/test/c123", ["/test/c123", "", "c123"]], ], [ [{ uid: "u123" }, "/test/u123"], @@ -2060,7 +2060,7 @@ const TESTS: Test[] = [ ], [ ["/foobaz", ["/foobaz", "foo"]], - ["/baz", ["/baz", undefined]], + ["/baz", ["/baz", ""]], ], [ [{}, "/baz"], @@ -2090,7 +2090,7 @@ const TESTS: Test[] = [ ], [ ["/hello(world)", ["/hello(world)", "hello", "world"]], - ["/hello()", ["/hello()", "hello", undefined]], + ["/hello()", ["/hello()", "hello", ""]], ], [ [{ foo: "hello", bar: "world" }, "/hello(world)"], @@ -2117,7 +2117,7 @@ const TESTS: Test[] = [ }, ], [ - ["/video", ["/video", "video", undefined]], + ["/video", ["/video", "video", ""]], ["/video+test", ["/video+test", "video", "+test"]], ["/video+", null], ], @@ -2545,7 +2545,7 @@ const TESTS: Test[] = [ }, ], [ - ["/user/123", ["/user/123", undefined, "123"]], + ["/user/123", ["/user/123", "", "123"]], ["/users/123", ["/users/123", "s", "123"]], ], [[{ user: "123" }, "/user/123"]], diff --git a/src/index.ts b/src/index.ts index 4454098..d68a208 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ +const DEFAULT_PREFIXES = "./"; +const DEFAULT_DELIMITER = "/#?"; +const DEFAULT_ENCODE = (x: string) => x; +const DEFAULT_DECODE = (x: string) => x; + /** * Tokenizer results. */ @@ -138,10 +143,11 @@ export interface ParseOptions { * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): Token[] { - const tokens = lexer(str); - const { prefixes = "./" } = options; - const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`; + const { prefixes = DEFAULT_PREFIXES, delimiter = DEFAULT_DELIMITER } = + options; + const defaultPattern = `[^${escapeString(delimiter)}]+?`; const result: Token[] = []; + const tokens = lexer(str); let key = 0; let i = 0; let path = ""; @@ -265,7 +271,7 @@ export function tokensToFunction

( options: TokensToFunctionOptions = {}, ): PathFunction

{ const reFlags = flags(options); - const { encode = (x: string) => x, validate = true } = options; + const { encode = DEFAULT_ENCODE, validate = true } = options; // Compile all the tokens into regexps. const matches = tokens.map((token) => { @@ -388,7 +394,7 @@ export function regexpToFunction

( keys: Key[], options: RegexpToFunctionOptions = {}, ): MatchFunction

{ - const { decode = (x: string) => x } = options; + const { decode = DEFAULT_DECODE } = options; return function (pathname: string) { const m = re.exec(pathname); @@ -452,19 +458,17 @@ function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp { if (!keys) return path; const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; - let index = 0; - let execResult = groupsRegex.exec(path.source); - while (execResult) { + let execResult: RegExpExecArray | null = null; + while ((execResult = groupsRegex.exec(path.source))) { keys.push({ - // Use parenthesized substring match if available, index otherwise + // Use parenthesized substring match if available, index otherwise. name: execResult[1] || index++, prefix: "", suffix: "", modifier: "", pattern: "", }); - execResult = groupsRegex.exec(path.source); } return path; @@ -536,8 +540,8 @@ export function tokensToRegexp( strict = false, start = true, end = true, - encode = (x: string) => x, - delimiter = "/#?", + encode = DEFAULT_ENCODE, + delimiter = DEFAULT_DELIMITER, endsWith = "", } = options; const endsWithRe = `[${escapeString(endsWith)}]|$`; @@ -563,11 +567,7 @@ export function tokensToRegexp( route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; } } else { - if (token.modifier === "+" || token.modifier === "*") { - route += `((?:${token.pattern})${token.modifier})`; - } else { - route += `(${token.pattern})${token.modifier}`; - } + route += `((?:${token.pattern})${token.modifier})`; } } else { route += `(?:${prefix}${suffix})${token.modifier}`; @@ -578,13 +578,13 @@ export function tokensToRegexp( if (end) { if (!strict) route += `${delimiterRe}?`; - route += !options.endsWith ? "$" : `(?=${endsWithRe})`; + route += options.endsWith ? `(?=${endsWithRe})` : "$"; } else { const endToken = tokens[tokens.length - 1]; const isEndDelimited = typeof endToken === "string" - ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 - : endToken === undefined; + ? delimiter.indexOf(endToken[endToken.length - 1]) > -1 + : !endToken; if (!strict) { route += `(?:${delimiterRe}(?=${endsWithRe}))?`; From 0b3aba6e6a13de41de46fa79eaa962dbc645249e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 19 May 2024 18:42:55 -0700 Subject: [PATCH 04/55] Reuse iter class > fns, less regex paths --- src/index.spec.ts | 22 ++++------ src/index.ts | 106 +++++++++++++++++++++++++--------------------- 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 8e041cf..4793b1a 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1215,7 +1215,7 @@ const TESTS: Test[] = [ [ ["route", ["route", "route"]], ["/route", null], - ["", ["", ""]], + ["", ["", undefined]], ["route/foobar", null], ], [ @@ -1888,9 +1888,9 @@ const TESTS: Test[] = [ ], [ ["/test", null], - ["/test/", ["/test/", "", ""]], - ["/test/u123", ["/test/u123", "u123", ""]], - ["/test/c123", ["/test/c123", "", "c123"]], + ["/test/", ["/test/", undefined, undefined]], + ["/test/u123", ["/test/u123", "u123", undefined]], + ["/test/c123", ["/test/c123", undefined, "c123"]], ], [ [{ uid: "u123" }, "/test/u123"], @@ -2060,7 +2060,7 @@ const TESTS: Test[] = [ ], [ ["/foobaz", ["/foobaz", "foo"]], - ["/baz", ["/baz", ""]], + ["/baz", ["/baz", undefined]], ], [ [{}, "/baz"], @@ -2090,7 +2090,7 @@ const TESTS: Test[] = [ ], [ ["/hello(world)", ["/hello(world)", "hello", "world"]], - ["/hello()", ["/hello()", "hello", ""]], + ["/hello()", ["/hello()", "hello", undefined]], ], [ [{ foo: "hello", bar: "world" }, "/hello(world)"], @@ -2117,7 +2117,7 @@ const TESTS: Test[] = [ }, ], [ - ["/video", ["/video", "video", ""]], + ["/video", ["/video", "video", undefined]], ["/video+test", ["/video+test", "video", "+test"]], ["/video+", null], ], @@ -2545,7 +2545,7 @@ const TESTS: Test[] = [ }, ], [ - ["/user/123", ["/user/123", "", "123"]], + ["/user/123", ["/user/123", undefined, "123"]], ["/users/123", ["/users/123", "s", "123"]], ], [[{ user: "123" }, "/user/123"]], @@ -2793,12 +2793,6 @@ describe("path-to-regexp", () => { pathToRegexp.pathToRegexp("/{a{b:foo}}"); }).toThrow(new TypeError("Unexpected OPEN at 3, expected CLOSE")); }); - - it("should throw on misplaced modifier", () => { - expect(() => { - pathToRegexp.pathToRegexp("/foo?"); - }).toThrow(new TypeError("Unexpected MODIFIER at 4, expected END")); - }); }); describe("tokens", () => { diff --git a/src/index.ts b/src/index.ts index d68a208..bf0ee41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ interface LexToken { /** * Tokenize input string. */ -function lexer(str: string): LexToken[] { +function lexer(str: string) { const tokens: LexToken[] = []; let i = 0; @@ -125,7 +125,7 @@ function lexer(str: string): LexToken[] { tokens.push({ type: "END", index: i, value: "" }); - return tokens; + return new Iter(tokens); } export interface ParseOptions { @@ -139,6 +139,41 @@ export interface ParseOptions { prefixes?: string; } +class Iter { + index = 0; + + constructor(private tokens: LexToken[]) {} + + peek(): LexToken { + return this.tokens[this.index]; + } + + tryConsume(type: LexToken["type"]): string | undefined { + const token = this.peek(); + if (token.type !== type) return; + this.index++; + return token.value; + } + + consume(type: LexToken["type"]): string { + const value = this.tryConsume(type); + if (value !== undefined) return value; + const { type: nextType, index } = this.peek(); + throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`); + } + + text(): string { + let result = ""; + let value: string | undefined; + while ( + (value = this.tryConsume("CHAR") || this.tryConsume("ESCAPED_CHAR")) + ) { + result += value; + } + return result; + } +} + /** * Parse a string for the raw tokens. */ @@ -149,33 +184,12 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { const result: Token[] = []; const tokens = lexer(str); let key = 0; - let i = 0; let path = ""; - const tryConsume = (type: LexToken["type"]): string | undefined => { - if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; - }; - - const mustConsume = (type: LexToken["type"]): string => { - const value = tryConsume(type); - if (value !== undefined) return value; - const { type: nextType, index } = tokens[i]; - throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`); - }; - - const consumeText = (): string => { - let result = ""; - let value: string | undefined; - while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { - result += value; - } - return result; - }; - - while (i < tokens.length) { - const char = tryConsume("CHAR"); - const name = tryConsume("NAME"); - const pattern = tryConsume("PATTERN"); + do { + const char = tokens.tryConsume("CHAR"); + const name = tokens.tryConsume("NAME"); + const pattern = tokens.tryConsume("PATTERN"); if (name || pattern) { let prefix = char || ""; @@ -195,12 +209,12 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { prefix, suffix: "", pattern: pattern || defaultPattern, - modifier: tryConsume("MODIFIER") || "", + modifier: tokens.tryConsume("MODIFIER") || "", }); continue; } - const value = char || tryConsume("ESCAPED_CHAR"); + const value = char || tokens.tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; @@ -211,27 +225,28 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { path = ""; } - const open = tryConsume("OPEN"); + const open = tokens.tryConsume("OPEN"); if (open) { - const prefix = consumeText(); - const name = tryConsume("NAME") || ""; - const pattern = tryConsume("PATTERN") || ""; - const suffix = consumeText(); + const prefix = tokens.text(); + const name = tokens.tryConsume("NAME") || ""; + const pattern = tokens.tryConsume("PATTERN") || ""; + const suffix = tokens.text(); - mustConsume("CLOSE"); + tokens.consume("CLOSE"); result.push({ name: name || (pattern ? key++ : ""), pattern: name && !pattern ? defaultPattern : pattern, prefix, suffix, - modifier: tryConsume("MODIFIER") || "", + modifier: tokens.tryConsume("MODIFIER") || "", }); continue; } - mustConsume("END"); - } + tokens.consume("END"); + break; + } while (true); return result; } @@ -559,15 +574,11 @@ export function tokensToRegexp( if (token.pattern) { if (keys) keys.push(token); - if (prefix || suffix) { - if (token.modifier === "+" || token.modifier === "*") { - const mod = token.modifier === "*" ? "?" : ""; - route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`; - } else { - route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; - } + if (token.modifier === "+" || token.modifier === "*") { + const mod = token.modifier === "*" ? "?" : ""; + route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`; } else { - route += `((?:${token.pattern})${token.modifier})`; + route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; } } else { route += `(?:${prefix}${suffix})${token.modifier}`; @@ -577,8 +588,7 @@ export function tokensToRegexp( if (end) { if (!strict) route += `${delimiterRe}?`; - - route += options.endsWith ? `(?=${endsWithRe})` : "$"; + route += endsWith ? `(?=${endsWithRe})` : "$"; } else { const endToken = tokens[tokens.length - 1]; const isEndDelimited = From 8b4d1fb24749ca4871adedc0162844f80f55878e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 19 May 2024 18:43:24 -0700 Subject: [PATCH 05/55] Allow standalone modifiers --- src/index.spec.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 12 ++++++++ 2 files changed, 85 insertions(+) diff --git a/src/index.spec.ts b/src/index.spec.ts index 4793b1a..6ccbe63 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1698,6 +1698,79 @@ const TESTS: Test[] = [ ], [[{ 0: "test" }, "/test"]], ], + /** + * Standalone modifiers. + */ + [ + "/*", + undefined, + [ + "/", + { + name: 0, + prefix: "", + suffix: "", + modifier: "", + pattern: ".*", + }, + ], + [ + ["/", ["/", ""]], + ["/route", ["/route", "route"]], + ["/route/nested", ["/route/nested", "route/nested"]], + ], + [ + [{ 0: "" }, "/"], + [{ 0: "123" }, "/123"], + ], + ], + [ + "/+", + undefined, + [ + "/", + { + name: 0, + prefix: "", + suffix: "", + modifier: "", + pattern: ".+", + }, + ], + [ + ["/", null], + ["/x", ["/x", "x"]], + ["/route", ["/route", "route"]], + ], + [ + [{ 0: "" }, null], + [{ 0: "x" }, "/x"], + [{ 0: "xyz" }, "/xyz"], + ], + ], + [ + "/?", + undefined, + [ + "/", + { + name: 0, + prefix: "", + suffix: "", + modifier: "", + pattern: ".?", + }, + ], + [ + ["/", ["/", ""]], + ["/x", ["/x", "x"]], + ["/route", null], + ], + [ + [{ 0: "" }, "/"], + [{ 0: "x" }, "/x"], + ], + ], /** * Regexps. diff --git a/src/index.ts b/src/index.ts index bf0ee41..a8308fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -244,6 +244,18 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { continue; } + const modifier = tokens.tryConsume("MODIFIER"); + if (modifier) { + result.push({ + name: key++, + prefix: "", + suffix: "", + pattern: `.${modifier}`, + modifier: "", + }); + continue; + } + tokens.consume("END"); break; } while (true); From 246ec9f6aa2cc2b90d249438ac053f57caecdf1b Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 20 May 2024 14:19:37 -0700 Subject: [PATCH 06/55] Standalone modifiers act like unnamed params --- src/index.spec.ts | 38 +++++++++++++++++++------------------- src/index.ts | 17 +++-------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 6ccbe63..e301598 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1705,69 +1705,69 @@ const TESTS: Test[] = [ "/*", undefined, [ - "/", { name: 0, - prefix: "", + prefix: "/", suffix: "", - modifier: "", - pattern: ".*", + modifier: "*", + pattern: "[^\\/#\\?]+?", }, ], [ - ["/", ["/", ""]], + ["/", ["/", undefined]], ["/route", ["/route", "route"]], ["/route/nested", ["/route/nested", "route/nested"]], ], [ - [{ 0: "" }, "/"], - [{ 0: "123" }, "/123"], + [{ 0: null }, ""], + [{ 0: "x" }, "/x"], + [{ 0: ["a", "b", "c"] }, "/a/b/c"], ], ], [ "/+", undefined, [ - "/", { name: 0, - prefix: "", + prefix: "/", suffix: "", - modifier: "", - pattern: ".+", + modifier: "+", + pattern: "[^\\/#\\?]+?", }, ], [ ["/", null], ["/x", ["/x", "x"]], ["/route", ["/route", "route"]], + ["/a/b/c", ["/a/b/c", "a/b/c"]], ], [ [{ 0: "" }, null], [{ 0: "x" }, "/x"], - [{ 0: "xyz" }, "/xyz"], + [{ 0: "route" }, "/route"], + [{ 0: ["a", "b", "c"] }, "/a/b/c"], ], ], [ "/?", undefined, [ - "/", { name: 0, - prefix: "", + prefix: "/", suffix: "", - modifier: "", - pattern: ".?", + modifier: "?", + pattern: "[^\\/#\\?]+?", }, ], [ - ["/", ["/", ""]], + ["/", ["/", undefined]], ["/x", ["/x", "x"]], - ["/route", null], + ["/route", ["/route", "route"]], ], [ - [{ 0: "" }, "/"], + [{ 0: undefined }, ""], [{ 0: "x" }, "/x"], ], ], diff --git a/src/index.ts b/src/index.ts index a8308fb..9f8332d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -190,8 +190,9 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { const char = tokens.tryConsume("CHAR"); const name = tokens.tryConsume("NAME"); const pattern = tokens.tryConsume("PATTERN"); + const modifier = tokens.tryConsume("MODIFIER"); - if (name || pattern) { + if (name || pattern || modifier) { let prefix = char || ""; if (prefixes.indexOf(prefix) === -1) { @@ -209,7 +210,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { prefix, suffix: "", pattern: pattern || defaultPattern, - modifier: tokens.tryConsume("MODIFIER") || "", + modifier: modifier || "", }); continue; } @@ -244,18 +245,6 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { continue; } - const modifier = tokens.tryConsume("MODIFIER"); - if (modifier) { - result.push({ - name: key++, - prefix: "", - suffix: "", - pattern: `.${modifier}`, - modifier: "", - }); - continue; - } - tokens.consume("END"); break; } while (true); From ee2d2cfad4388c6c85d45e55ae9ba81aadaece1a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 20 May 2024 15:24:45 -0700 Subject: [PATCH 07/55] Encode parser tokens --- package.json | 2 +- src/index.spec.ts | 10 ++++---- src/index.ts | 61 ++++++++++++++++++++++++++++------------------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index df50fe3..d990da1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "size-limit": [ { "path": "dist.es2015/index.js", - "limit": "2 kB" + "limit": "2.1 kB" } ], "ts-scripts": { diff --git a/src/index.spec.ts b/src/index.spec.ts index e301598..3e71f3a 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -191,7 +191,7 @@ const TESTS: Test[] = [ [{}, null], [{ test: "abc" }, "/abc"], [{ test: "a+b" }, "/a+b"], - [{ test: "a+b" }, "/test", { encode: (_, token) => String(token.name) }], + [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], ], @@ -285,7 +285,7 @@ const TESTS: Test[] = [ [{}, null], [{ test: "abc" }, "/abc"], [{ test: "a+b" }, "/a+b"], - [{ test: "a+b" }, "/test", { encode: (_, token) => String(token.name) }], + [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], ], @@ -2285,10 +2285,10 @@ const TESTS: Test[] = [ ["/café", undefined, ["/café"], [["/café", ["/café"]]], [[null, "/café"]]], [ "/café", - { encode: encodeURI }, - ["/café"], + { encode: encodeURIComponent }, + ["/caf%C3%A9"], [["/caf%C3%A9", ["/caf%C3%A9"]]], - [[null, "/café"]], + [[null, "/caf%C3%A9"]], ], [ "packages/", diff --git a/src/index.ts b/src/index.ts index 9f8332d..64ba927 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,6 +137,10 @@ export interface ParseOptions { * List of characters to automatically consider prefixes when parsing. */ prefixes?: string; + /** + * Function for encoding input strings for output into path. + */ + encode?: Encode; } class Iter { @@ -178,11 +182,15 @@ class Iter { * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): Token[] { - const { prefixes = DEFAULT_PREFIXES, delimiter = DEFAULT_DELIMITER } = - options; - const defaultPattern = `[^${escapeString(delimiter)}]+?`; + const { + prefixes = DEFAULT_PREFIXES, + delimiter = DEFAULT_DELIMITER, + encode = DEFAULT_ENCODE, + } = options; + const defaultPattern = `[^${escape(delimiter)}]+?`; const result: Token[] = []; const tokens = lexer(str); + const stringify = encoder(delimiter, encode); let key = 0; let path = ""; @@ -207,7 +215,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { result.push({ name: name || key++, - prefix, + prefix: stringify(prefix), suffix: "", pattern: pattern || defaultPattern, modifier: modifier || "", @@ -222,7 +230,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { } if (path) { - result.push(path); + result.push(stringify(path)); path = ""; } @@ -238,8 +246,8 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { result.push({ name: name || (pattern ? key++ : ""), pattern: name && !pattern ? defaultPattern : pattern, - prefix, - suffix, + prefix: stringify(prefix), + suffix: stringify(suffix), modifier: tokens.tryConsume("MODIFIER") || "", }); continue; @@ -252,19 +260,21 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { return result; } +export type Encode = (value: string) => string; + export interface TokensToFunctionOptions { /** * When `true` the regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; - /** - * Function for encoding input strings for output. - */ - encode?: (value: string, token: Key) => string; /** * When `false` the function can produce an invalid (unmatched) path. (default: `true`) */ validate?: boolean; + /** + * Function for encoding input strings for output. + */ + encode?: Encode; } /** @@ -325,7 +335,7 @@ export function tokensToFunction

( } for (let j = 0; j < value.length; j++) { - const segment = encode(value[j], token); + const segment = encode(value[j]); if (validate && !(matches[i] as RegExp).test(segment)) { throw new TypeError( @@ -340,7 +350,7 @@ export function tokensToFunction

( } if (typeof value === "string" || typeof value === "number") { - const segment = encode(String(value), token); + const segment = encode(String(value)); if (validate && !(matches[i] as RegExp).test(segment)) { throw new TypeError( @@ -440,10 +450,18 @@ export function regexpToFunction

( /** * Escape a regular expression string. */ -function escapeString(str: string) { +function escape(str: string) { return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); } +/** + * Encode all non-delimiter characters using the encode function. + */ +function encoder(delimiter: string, encode: Encode) { + const re = new RegExp(`[^${escape(delimiter)}]+`, "g"); + return (value: string) => value.replace(re, encode); +} + /** * Get the flags for a regexp from the options. */ @@ -538,10 +556,6 @@ export interface TokensToRegexpOptions { * List of characters that can also be "end" characters. */ endsWith?: string; - /** - * Encode path tokens for use in the `RegExp`. - */ - encode?: (value: string) => string; } /** @@ -556,21 +570,20 @@ export function tokensToRegexp( strict = false, start = true, end = true, - encode = DEFAULT_ENCODE, delimiter = DEFAULT_DELIMITER, endsWith = "", } = options; - const endsWithRe = `[${escapeString(endsWith)}]|$`; - const delimiterRe = `[${escapeString(delimiter)}]`; + const endsWithRe = `[${escape(endsWith)}]|$`; + const delimiterRe = `[${escape(delimiter)}]`; let route = start ? "^" : ""; // Iterate over the tokens and create our regexp string. for (const token of tokens) { if (typeof token === "string") { - route += escapeString(encode(token)); + route += escape(token); } else { - const prefix = escapeString(encode(token.prefix)); - const suffix = escapeString(encode(token.suffix)); + const prefix = escape(token.prefix); + const suffix = escape(token.suffix); if (token.pattern) { if (keys) keys.push(token); From 9085edae1c5b06c7a4c55139412c68885ce5e5f6 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 21 May 2024 10:11:12 -0700 Subject: [PATCH 08/55] Remove es2015 bundle, bump node version --- package-lock.json | 4 +++- package.json | 18 ++++++++---------- tsconfig.es2015.json | 8 -------- tsconfig.json | 8 ++++---- 4 files changed, 15 insertions(+), 23 deletions(-) delete mode 100644 tsconfig.es2015.json diff --git a/package-lock.json b/package-lock.json index eeeb609..2eddca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,11 @@ "@types/node": "^20.4.9", "@types/semver": "^7.3.1", "@vitest/coverage-v8": "^1.4.0", - "semver": "^7.3.5", "size-limit": "^11.1.2", "typescript": "^5.1.6" + }, + "engines": { + "node": ">=16" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index d990da1..ed066c7 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,10 @@ "url": "https://github.com/pillarjs/path-to-regexp.git" }, "license": "MIT", - "sideEffects": false, + "exports": "./dist/index.js", "main": "dist/index.js", - "module": "dist.es2015/index.js", "typings": "dist/index.d.ts", "files": [ - "dist.es2015/", "dist/" ], "scripts": { @@ -36,27 +34,27 @@ "@types/node": "^20.4.9", "@types/semver": "^7.3.1", "@vitest/coverage-v8": "^1.4.0", - "semver": "^7.3.5", "size-limit": "^11.1.2", "typescript": "^5.1.6" }, + "engines": { + "node": ">=16" + }, "publishConfig": { "access": "public" }, "size-limit": [ { - "path": "dist.es2015/index.js", - "limit": "2.1 kB" + "path": "dist/index.js", + "limit": "2.2 kB" } ], "ts-scripts": { "dist": [ - "dist", - "dist.es2015" + "dist" ], "project": [ - "tsconfig.build.json", - "tsconfig.es2015.json" + "tsconfig.build.json" ] } } diff --git a/tsconfig.es2015.json b/tsconfig.es2015.json deleted file mode 100644 index fa546d4..0000000 --- a/tsconfig.es2015.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.build.json", - "compilerOptions": { - "outDir": "dist.es2015", - "module": "es2015", - "declaration": false - } -} diff --git a/tsconfig.json b/tsconfig.json index a81e6f2..03b0dd0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "extends": "@borderless/ts-scripts/configs/tsconfig.json", "compilerOptions": { - "target": "es5", - "lib": ["es5"], + "target": "ES2015", + "lib": ["ES2015"], "rootDir": "src", "outDir": "dist", - "module": "commonjs", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "types": ["node"] }, "include": ["src/**/*"] From 55fbd0275cad22fb729e6c7e140f3fc16eae1028 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 21 May 2024 10:23:48 -0700 Subject: [PATCH 09/55] Add loose mode for repeatable delimiters --- src/index.spec.ts | 159 ++++++++++++++++++++++------------------------ src/index.ts | 124 ++++++++++++++++++++++-------------- 2 files changed, 154 insertions(+), 129 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 3e71f3a..2ea29e6 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from "vitest"; import * as util from "util"; import * as pathToRegexp from "./index"; -import semver from "semver"; type Test = [ pathToRegexp.Path, @@ -2703,96 +2702,92 @@ const TESTS: Test[] = [ [["foobar", ["foobar", "foobar"]]], [[{ name: "foobar" }, "foobar"]], ], -]; -/** - * Named capturing groups (available from 1812 version 10) - */ -if (semver.gte(process.version, "10.0.0")) { - TESTS.push( + /** + * Named capturing groups (available from 1812 version 10) + */ + [ + /\/(?.+)/, + undefined, [ - /\/(?.+)/, - undefined, - [ - { - name: "groupname", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/", null], - ["/foo", ["/foo", "foo"]], - ], - [], + { + name: "groupname", + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, ], [ - /\/(?.*).(?html|json)/, - undefined, - [ - { - name: "test", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: "format", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/route", null], - ["/route.txt", null], - ["/route.html", ["/route.html", "route", "html"]], - ["/route.json", ["/route.json", "route", "json"]], - ], - [], + ["/", null], + ["/foo", ["/foo", "foo"]], ], + [], + ], + [ + /\/(?.*).(?html|json)/, + undefined, [ - /\/(.+)\/(?.+)\/(.+)/, - undefined, - [ - { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: "groupname", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: 1, - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], + { + name: "test", + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, + { + name: "format", + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, + ], + [ + ["/route", null], + ["/route.txt", null], + ["/route.html", ["/route.html", "route", "html"]], + ["/route.json", ["/route.json", "route", "json"]], + ], + [], + ], + [ + /\/(.+)\/(?.+)\/(.+)/, + undefined, + [ + { + name: 0, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, + { + name: "groupname", + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, + { + name: 1, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }, + ], + [ + ["/test", null], + ["/test/testData", null], [ - ["/test", null], - ["/test/testData", null], - [ - "/test/testData/extraStuff", - ["/test/testData/extraStuff", "test", "testData", "extraStuff"], - ], + "/test/testData/extraStuff", + ["/test/testData/extraStuff", "test", "testData", "extraStuff"], ], - [], ], - ); -} + [], + ], +]; /** * Dynamically generate the entire test suite. diff --git a/src/index.ts b/src/index.ts index 64ba927..83369fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ const DEFAULT_PREFIXES = "./"; -const DEFAULT_DELIMITER = "/#?"; -const DEFAULT_ENCODE = (x: string) => x; -const DEFAULT_DECODE = (x: string) => x; +const DEFAULT_DELIMITERS = "/#?"; +const NOOP_ENCODE = (x: string) => x; +const NOOP_DECODE = (x: string) => x; /** * Tokenizer results. @@ -184,13 +184,13 @@ class Iter { export function parse(str: string, options: ParseOptions = {}): Token[] { const { prefixes = DEFAULT_PREFIXES, - delimiter = DEFAULT_DELIMITER, - encode = DEFAULT_ENCODE, + delimiter = DEFAULT_DELIMITERS, + encode = NOOP_ENCODE, } = options; const defaultPattern = `[^${escape(delimiter)}]+?`; const result: Token[] = []; const tokens = lexer(str); - const stringify = encoder(delimiter, encode); + const stringify = encoder(delimiter, encode, NOOP_ENCODE); let key = 0; let path = ""; @@ -209,7 +209,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { } if (path) { - result.push(path); + result.push(stringify(path)); path = ""; } @@ -297,7 +297,7 @@ export function tokensToFunction

( options: TokensToFunctionOptions = {}, ): PathFunction

{ const reFlags = flags(options); - const { encode = DEFAULT_ENCODE, validate = true } = options; + const { encode = NOOP_ENCODE, validate = true } = options; // Compile all the tokens into regexps. const matches = tokens.map((token) => { @@ -306,7 +306,7 @@ export function tokensToFunction

( } }); - return (data: Record | null | undefined) => { + return function path(data: Record | undefined) { let path = ""; for (let i = 0; i < tokens.length; i++) { @@ -372,13 +372,6 @@ export function tokensToFunction

( }; } -export interface RegexpToFunctionOptions { - /** - * Function for decoding strings for params. - */ - decode?: (value: string, token: Key) => string; -} - /** * A match result contains data about the path match. */ @@ -412,6 +405,13 @@ export function match

( return regexpToFunction

(re, keys, options); } +export interface RegexpToFunctionOptions { + /** + * Function for decoding strings for params. + */ + decode?: (value: string, token: Key) => string; +} + /** * Create a path match function from `path-to-regexp` output. */ @@ -420,9 +420,18 @@ export function regexpToFunction

( keys: Key[], options: RegexpToFunctionOptions = {}, ): MatchFunction

{ - const { decode = DEFAULT_DECODE } = options; + const { decode = NOOP_DECODE } = options; + const decoders = keys.map((key) => { + if (key.split) { + const splitRe = new RegExp(key.split, "g"); + return (value: string, key: Key) => + value.split(splitRe).map((part) => decode(part, key)); + } + + return (value: string, key: Key) => decode(value, key); + }); - return function (pathname: string) { + return function match(pathname: string) { const m = re.exec(pathname); if (!m) return false; @@ -433,14 +442,8 @@ export function regexpToFunction

( if (m[i] === undefined) continue; const key = keys[i - 1]; - - if (key.modifier === "*" || key.modifier === "+") { - params[key.name] = m[i].split(key.prefix + key.suffix).map((value) => { - return decode(value, key); - }); - } else { - params[key.name] = decode(m[i], key); - } + const decoder = decoders[i - 1]; + params[key.name] = decoder(m[i], key); } return { path, index, params }; @@ -454,12 +457,25 @@ function escape(str: string) { return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); } +/** + * Escape and repeat a string for regular expressions. + */ +function repeat(str: string) { + return `${escape(str)}+`; +} + /** * Encode all non-delimiter characters using the encode function. */ -function encoder(delimiter: string, encode: Encode) { - const re = new RegExp(`[^${escape(delimiter)}]+`, "g"); - return (value: string) => value.replace(re, encode); +function encoder( + delimiter: string, + encodeString: Encode, + encodeDelimiter: Encode, +) { + const re = new RegExp(`[^${escape(delimiter)}]+|(.)`, "g"); + const replacer = (value: string, delimiter: string) => + delimiter ? encodeDelimiter(value) : encodeString(value); + return (value: string) => value.replace(re, replacer); } /** @@ -469,10 +485,7 @@ function flags(options?: { sensitive?: boolean }) { return options && options.sensitive ? "" : "i"; } -/** - * Metadata about a key. - */ -export interface Key { +export interface TokenKey { name: string | number; prefix: string; suffix: string; @@ -483,7 +496,17 @@ export interface Key { /** * A token is a string (nothing special) or key metadata (capture group). */ -export type Token = string | Key; +export type Token = string | TokenKey; + +/** + * Metadata about a key. + */ +export interface Key extends TokenKey { + /** + * Internal flag indicating the key needs to be split for the match. + */ + split?: string; +} /** * Pull out keys from a regexp. @@ -556,6 +579,10 @@ export interface TokensToRegexpOptions { * List of characters that can also be "end" characters. */ endsWith?: string; + /** + * When `true` the regexp will allow repeated delimiters. (default: `false`) + */ + loose?: boolean; } /** @@ -563,35 +590,38 @@ export interface TokensToRegexpOptions { */ export function tokensToRegexp( tokens: Token[], - keys?: Key[], + keys: Key[] = [], options: TokensToRegexpOptions = {}, -) { +): RegExp { const { strict = false, + loose = false, start = true, end = true, - delimiter = DEFAULT_DELIMITER, + delimiter = DEFAULT_DELIMITERS, endsWith = "", } = options; - const endsWithRe = `[${escape(endsWith)}]|$`; + const endsWithRe = endsWith ? `[${escape(endsWith)}]|$` : "$"; const delimiterRe = `[${escape(delimiter)}]`; + const stringify = loose ? encoder(delimiter, escape, repeat) : escape; let route = start ? "^" : ""; // Iterate over the tokens and create our regexp string. for (const token of tokens) { if (typeof token === "string") { - route += escape(token); + route += stringify(token); } else { - const prefix = escape(token.prefix); - const suffix = escape(token.suffix); + const prefix = stringify(token.prefix); + const suffix = stringify(token.suffix); if (token.pattern) { - if (keys) keys.push(token); - if (token.modifier === "+" || token.modifier === "*") { const mod = token.modifier === "*" ? "?" : ""; - route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`; + const split = `${suffix}${prefix}`; + keys.push({ ...token, split }); + route += `(?:${prefix}((?:${token.pattern})(?:${split}(?:${token.pattern}))*)${suffix})${mod}`; } else { + keys.push(token); route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; } } else { @@ -601,8 +631,8 @@ export function tokensToRegexp( } if (end) { - if (!strict) route += `${delimiterRe}?`; - route += endsWith ? `(?=${endsWithRe})` : "$"; + if (!strict) route += `${delimiterRe}${loose ? "*" : "?"}`; + route += `(?=${endsWithRe})`; } else { const endToken = tokens[tokens.length - 1]; const isEndDelimited = @@ -611,7 +641,7 @@ export function tokensToRegexp( : !endToken; if (!strict) { - route += `(?:${delimiterRe}(?=${endsWithRe}))?`; + route += `(?:${delimiterRe}${loose ? "+" : ""}(?=${endsWithRe}))?`; } if (!isEndDelimited) { From 44a51416c3eeff6ae3391b46e0945bbac02b45cf Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 21 May 2024 10:24:52 -0700 Subject: [PATCH 10/55] Remove key from decode type --- src/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 83369fe..142a2a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -409,7 +409,7 @@ export interface RegexpToFunctionOptions { /** * Function for decoding strings for params. */ - decode?: (value: string, token: Key) => string; + decode?: (value: string) => string; } /** @@ -424,11 +424,10 @@ export function regexpToFunction

( const decoders = keys.map((key) => { if (key.split) { const splitRe = new RegExp(key.split, "g"); - return (value: string, key: Key) => - value.split(splitRe).map((part) => decode(part, key)); + return (value: string) => value.split(splitRe).map(decode); } - return (value: string, key: Key) => decode(value, key); + return decode; }); return function match(pathname: string) { @@ -443,7 +442,7 @@ export function regexpToFunction

( const key = keys[i - 1]; const decoder = decoders[i - 1]; - params[key.name] = decoder(m[i], key); + params[key.name] = decoder(m[i]); } return { path, index, params }; From 3574a3ef6296c59769de096b243dfc748ac80208 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 21 May 2024 14:26:56 -0700 Subject: [PATCH 11/55] Remove `endsWith`, rename `strict`, simply regex --- package.json | 2 +- src/index.spec.ts | 258 +++++++++++++++++----------------------------- src/index.ts | 138 +++++++++++-------------- tsconfig.json | 4 +- 4 files changed, 156 insertions(+), 246 deletions(-) diff --git a/package.json b/package.json index ed066c7..d914b63 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "size-limit": [ { "path": "dist/index.js", - "limit": "2.2 kB" + "limit": "2.1 kB" } ], "ts-scripts": { diff --git a/src/index.spec.ts b/src/index.spec.ts index 2ea29e6..d9673f8 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -99,7 +99,7 @@ const TESTS: Test[] = [ [ "/test", { - strict: true, + trailing: false, }, ["/test"], [ @@ -112,13 +112,14 @@ const TESTS: Test[] = [ [ "/test/", { - strict: true, + trailing: false, }, ["/test/"], [ ["/test", null], ["/test/", ["/test/"]], - ["/test//", null], + ["/test//", ["/test//"]], + ["/test/route", null], ], [[null, "/test/"]], ], @@ -148,9 +149,10 @@ const TESTS: Test[] = [ ["/test/"], [ ["/test", null], - ["/test/route", ["/test/"]], - ["/test//", ["/test//"]], + ["/test/route", null], ["/test//route", ["/test/"]], + ["/test//", ["/test//"]], + ["/foo//bar", null], ], [[null, "/test/"]], ], @@ -165,7 +167,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -205,7 +207,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], @@ -224,7 +226,7 @@ const TESTS: Test[] = [ [ ["", [""]], ["/", ["/"]], - ["route", [""]], + ["route", null], ["/route", [""]], ["/route/", [""]], ], @@ -276,7 +278,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [["/route", ["/route", "route"]]], @@ -299,7 +301,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], @@ -332,7 +334,7 @@ const TESTS: Test[] = [ "/test", { end: false, - strict: true, + trailing: false, }, ["/test"], [ @@ -346,14 +348,15 @@ const TESTS: Test[] = [ "/test/", { end: false, - strict: true, + trailing: false, }, ["/test/"], [ ["/test", null], ["/test/", ["/test/"]], - ["/test//", ["/test/"]], - ["/test/route", ["/test/"]], + ["/test//", ["/test//"]], + ["/test/route", null], + ["/test//route", ["/test/"]], ], [[null, "/test/"]], ], @@ -361,7 +364,7 @@ const TESTS: Test[] = [ "/test.json", { end: false, - strict: true, + trailing: false, }, ["/test.json"], [ @@ -375,7 +378,7 @@ const TESTS: Test[] = [ "/:test", { end: false, - strict: true, + trailing: false, }, [ { @@ -383,7 +386,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -399,7 +402,7 @@ const TESTS: Test[] = [ "/:test/", { end: false, - strict: true, + trailing: false, }, [ { @@ -407,7 +410,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], @@ -443,8 +446,8 @@ const TESTS: Test[] = [ ["/test", null], ["/test/", ["/test/"]], ["/test//", ["/test//"]], - ["/test/route", ["/test/"]], - ["/route/test/deep", ["/test/"]], + ["/test/route", null], + ["/route/test/deep", null], ], [[null, "/test/"]], ], @@ -475,7 +478,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -491,7 +494,7 @@ const TESTS: Test[] = [ "/:test/", { end: false, - strict: true, + trailing: false, }, [ { @@ -499,7 +502,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], @@ -551,7 +554,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -583,7 +586,7 @@ const TESTS: Test[] = [ [ "/:test", { - strict: true, + trailing: false, }, [ { @@ -591,7 +594,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -603,7 +606,7 @@ const TESTS: Test[] = [ [ "/:test/", { - strict: true, + trailing: false, }, [ { @@ -611,13 +614,13 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], [ ["/route/", ["/route/", "route"]], - ["/route//", null], + ["/route//", ["/route//", "route"]], ], [[{ test: "route" }, "/route/"]], ], @@ -632,12 +635,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ ["/route.json", ["/route.json", "route.json"]], - ["/route//", ["/route", "route"]], + ["/route//", ["/route//", "route"]], + ["/foo/bar", ["/foo", "foo"]], + ["/foo//bar", ["/foo/", "foo"]], ], [[{ test: "route" }, "/route"]], ], @@ -654,7 +659,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -665,7 +670,7 @@ const TESTS: Test[] = [ ], ["/route/nested", null, false], ["/", ["/", undefined], { path: "/", index: 0, params: {} }], - ["//", null], + ["//", ["//", undefined], { path: "//", index: 0, params: {} }], ], [ [null, ""], @@ -675,7 +680,7 @@ const TESTS: Test[] = [ [ "/:test?", { - strict: true, + trailing: false, }, [ { @@ -683,7 +688,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -699,7 +704,7 @@ const TESTS: Test[] = [ [ "/:test?/", { - strict: true, + trailing: false, }, [ { @@ -707,7 +712,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/", ], @@ -715,7 +720,7 @@ const TESTS: Test[] = [ ["/route", null], ["/route/", ["/route/", "route"]], ["/", ["/", undefined]], - ["//", null], + ["//", ["//", undefined]], ], [ [null, "/"], @@ -731,7 +736,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "/bar", ], @@ -753,7 +758,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "-bar", ], @@ -776,7 +781,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "*", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "-bar", ], @@ -801,7 +806,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -910,12 +915,12 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "*", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ ["/", ["/", undefined], { path: "/", index: 0, params: {} }], - ["//", null, false], + ["//", ["//", undefined], { path: "//", index: 0, params: {} }], [ "/route", ["/route", "route"], @@ -1141,7 +1146,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1159,7 +1164,7 @@ const TESTS: Test[] = [ [ ":test", { - strict: true, + trailing: false, }, [ { @@ -1167,7 +1172,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1188,7 +1193,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1208,7 +1213,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1232,7 +1237,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "/", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1271,7 +1276,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ".json", ], @@ -1352,7 +1357,7 @@ const TESTS: Test[] = [ prefix: ".", suffix: "", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1396,7 +1401,7 @@ const TESTS: Test[] = [ prefix: ".", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ".", ], @@ -1422,14 +1427,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "format", prefix: ".", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1451,14 +1456,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "format", prefix: ".", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1483,14 +1488,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "format", prefix: ".", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1709,7 +1714,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "*", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1732,7 +1737,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1757,7 +1762,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -1865,14 +1870,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "test", prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -2015,14 +2020,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "bar", prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [["/match/route", ["/match/route", "match", "route"]]], @@ -2037,7 +2042,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "(test)/bar", ], @@ -2085,7 +2090,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "?", ], @@ -2101,7 +2106,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "baz", ], @@ -2126,7 +2131,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "baz", ], @@ -2148,7 +2153,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "(", { @@ -2156,7 +2161,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ")", ], @@ -2207,14 +2212,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "bar", prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "-ext", ], @@ -2241,14 +2246,14 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, { name: "optional", prefix: "/", suffix: "", modifier: "?", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, "-ext", ], @@ -2272,7 +2277,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [["/café", ["/café", "café"]]], @@ -2362,7 +2367,7 @@ const TESTS: Test[] = [ prefix: ".", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -2388,37 +2393,6 @@ const TESTS: Test[] = [ [[null, "this is"]], ], - /** - * Ends with. - */ - [ - "/test", - { - endsWith: "?", - }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test?query=string", ["/test"]], - ["/test/?query=string", ["/test/"]], - ["/testx", null], - ], - [[null, "/test"]], - ], - [ - "/test", - { - endsWith: "?", - strict: true, - }, - ["/test"], - [ - ["/test?query=string", ["/test"]], - ["/test/?query=string", null], - ], - [[null, "/test"]], - ], - /** * Custom prefixes. */ @@ -2428,14 +2402,14 @@ const TESTS: Test[] = [ [ { name: "foo", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", prefix: "$", suffix: "", modifier: "", }, { name: "bar", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", prefix: "$", suffix: "", modifier: "?", @@ -2457,21 +2431,21 @@ const TESTS: Test[] = [ "name", { name: "attr1", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", prefix: "/", suffix: "", modifier: "?", }, { name: "attr2", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", prefix: "-", suffix: "", modifier: "?", }, { name: "attr3", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", prefix: "-", suffix: "", modifier: "?", @@ -2613,7 +2587,7 @@ const TESTS: Test[] = [ prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [ @@ -2623,52 +2597,6 @@ const TESTS: Test[] = [ [[{ user: "123" }, "/user/123"]], ], - /** - * https://github.com/pillarjs/path-to-regexp/issues/209 - */ - [ - "/whatever/:foo\\?query=str", - undefined, - [ - "/whatever", - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/#\\?]+?", - }, - "?query=str", - ], - [["/whatever/123?query=str", ["/whatever/123?query=str", "123"]]], - [[{ foo: "123" }, "/whatever/123?query=str"]], - ], - [ - "/whatever/:foo", - { - end: false, - }, - [ - "/whatever", - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/#\\?]+?", - }, - ], - [ - ["/whatever/123", ["/whatever/123", "123"]], - ["/whatever/123/path", ["/whatever/123", "123"]], - ["/whatever/123#fragment", ["/whatever/123", "123"]], - ["/whatever/123?query=str", ["/whatever/123", "123"]], - ], - [ - [{ foo: "123" }, "/whatever/123"], - [{ foo: "#" }, null], - ], - ], /** * https://github.com/pillarjs/path-to-regexp/issues/260 */ @@ -2681,7 +2609,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "*", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [["foobar", ["foobar", "foobar"]]], @@ -2696,7 +2624,7 @@ const TESTS: Test[] = [ prefix: "", suffix: "", modifier: "+", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }, ], [["foobar", ["foobar", "foobar"]]], @@ -2800,7 +2728,7 @@ describe("path-to-regexp", () => { prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/#\\?]+?", + pattern: "[^\\/]+?", }; describe("arguments", () => { diff --git a/src/index.ts b/src/index.ts index 142a2a0..28f7357 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ const DEFAULT_PREFIXES = "./"; -const DEFAULT_DELIMITERS = "/#?"; +const DEFAULT_DELIMITER = "/"; const NOOP_ENCODE = (x: string) => x; const NOOP_DECODE = (x: string) => x; +const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; + /** * Tokenizer results. */ @@ -128,21 +130,6 @@ function lexer(str: string) { return new Iter(tokens); } -export interface ParseOptions { - /** - * Set the default delimiter for repeat parameters. (default: `'/'`) - */ - delimiter?: string; - /** - * List of characters to automatically consider prefixes when parsing. - */ - prefixes?: string; - /** - * Function for encoding input strings for output into path. - */ - encode?: Encode; -} - class Iter { index = 0; @@ -178,13 +165,28 @@ class Iter { } } +export interface ParseOptions { + /** + * Set the default delimiter for repeat parameters. (default: `'/'`) + */ + delimiter?: string; + /** + * List of characters to automatically consider prefixes when parsing. + */ + prefixes?: string; + /** + * Function for encoding input strings for output into path. + */ + encode?: Encode; +} + /** * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): Token[] { const { prefixes = DEFAULT_PREFIXES, - delimiter = DEFAULT_DELIMITERS, + delimiter = DEFAULT_DELIMITER, encode = NOOP_ENCODE, } = options; const defaultPattern = `[^${escape(delimiter)}]+?`; @@ -203,7 +205,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { if (name || pattern || modifier) { let prefix = char || ""; - if (prefixes.indexOf(prefix) === -1) { + if (!prefixes.includes(prefix)) { path += prefix; prefix = ""; } @@ -281,16 +283,16 @@ export interface TokensToFunctionOptions { * Compile a string to a template function for the path. */ export function compile

( - str: string, + value: string, options?: ParseOptions & TokensToFunctionOptions, ) { - return tokensToFunction

(parse(str, options), options); + return tokensToFunction

(parse(value, options), options); } export type PathFunction

= (data?: P) => string; /** - * Expose a method for transforming tokens into the path function. + * Transform tokens into a path building function. */ export function tokensToFunction

( tokens: Token[], @@ -423,8 +425,12 @@ export function regexpToFunction

( const { decode = NOOP_DECODE } = options; const decoders = keys.map((key) => { if (key.split) { - const splitRe = new RegExp(key.split, "g"); - return (value: string) => value.split(splitRe).map(decode); + const re = new RegExp(`(${key.pattern})(?:${key.split}|$)`, "g"); + return (value: string) => { + const result: string[] = []; + for (const m of value.matchAll(re)) result.push(decode(m[1])); + return result; + }; } return decode; @@ -480,8 +486,8 @@ function encoder( /** * Get the flags for a regexp from the options. */ -function flags(options?: { sensitive?: boolean }) { - return options && options.sensitive ? "" : "i"; +function flags(options: { sensitive?: boolean }) { + return options.sensitive ? "" : "i"; } export interface TokenKey { @@ -510,13 +516,11 @@ export interface Key extends TokenKey { /** * Pull out keys from a regexp. */ -function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp { +function regexpToRegexp(path: RegExp, keys: Key[]): RegExp { if (!keys) return path; - const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; let index = 0; - let execResult: RegExpExecArray | null = null; - while ((execResult = groupsRegex.exec(path.source))) { + for (const execResult of path.source.matchAll(GROUPS_RE)) { keys.push({ // Use parenthesized substring match if available, index otherwise. name: execResult[1] || index++, @@ -535,8 +539,8 @@ function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp { */ function arrayToRegexp( paths: Array, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, + keys: Key[], + options: TokensToRegexpOptions & ParseOptions, ): RegExp { const parts = paths.map((path) => pathToRegexp(path, keys, options).source); return new RegExp(`(?:${parts.join("|")})`, flags(options)); @@ -547,8 +551,8 @@ function arrayToRegexp( */ function stringToRegexp( path: string, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, + keys: Key[], + options: TokensToRegexpOptions & ParseOptions, ) { return tokensToRegexp(parse(path, options), keys, options); } @@ -559,9 +563,13 @@ export interface TokensToRegexpOptions { */ sensitive?: boolean; /** - * When `true` the regexp won't allow an optional trailing delimiter to match. (default: `false`) + * When `true` the regexp allows an optional trailing delimiter to match. (default: `true`) */ - strict?: boolean; + trailing?: boolean; + /** + * When `true` all delimiters can be repeated one or more times. (default: `true`) + */ + loose?: boolean; /** * When `true` the regexp will match to the end of the string. (default: `true`) */ @@ -571,17 +579,9 @@ export interface TokensToRegexpOptions { */ start?: boolean; /** - * Sets the final character for non-ending optimistic matches. (default: `/`) + * Sets the final character for non-ending optimistic matches. (default: `"/"`) */ delimiter?: string; - /** - * List of characters that can also be "end" characters. - */ - endsWith?: string; - /** - * When `true` the regexp will allow repeated delimiters. (default: `false`) - */ - loose?: boolean; } /** @@ -593,22 +593,20 @@ export function tokensToRegexp( options: TokensToRegexpOptions = {}, ): RegExp { const { - strict = false, - loose = false, + trailing = true, + loose = true, start = true, end = true, - delimiter = DEFAULT_DELIMITERS, - endsWith = "", + delimiter = DEFAULT_DELIMITER, } = options; - const endsWithRe = endsWith ? `[${escape(endsWith)}]|$` : "$"; - const delimiterRe = `[${escape(delimiter)}]`; + const delimiterRe = escape(delimiter); const stringify = loose ? encoder(delimiter, escape, repeat) : escape; - let route = start ? "^" : ""; + let pattern = start ? "^" : ""; // Iterate over the tokens and create our regexp string. for (const token of tokens) { if (typeof token === "string") { - route += stringify(token); + pattern += stringify(token); } else { const prefix = stringify(token.prefix); const suffix = stringify(token.suffix); @@ -616,39 +614,23 @@ export function tokensToRegexp( if (token.pattern) { if (token.modifier === "+" || token.modifier === "*") { const mod = token.modifier === "*" ? "?" : ""; - const split = `${suffix}${prefix}`; - keys.push({ ...token, split }); - route += `(?:${prefix}((?:${token.pattern})(?:${split}(?:${token.pattern}))*)${suffix})${mod}`; + const split = `${suffix}${prefix}` || delimiterRe; // Fallback to split on delimiter. + keys.push(Object.assign({}, token, { split })); + pattern += `(?:${prefix}((?:${token.pattern})(?:${split}(?:${token.pattern}))*)${suffix})${mod}`; } else { keys.push(token); - route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; + pattern += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; } } else { - route += `(?:${prefix}${suffix})${token.modifier}`; + pattern += `(?:${prefix}${suffix})${token.modifier}`; } } } - if (end) { - if (!strict) route += `${delimiterRe}${loose ? "*" : "?"}`; - route += `(?=${endsWithRe})`; - } else { - const endToken = tokens[tokens.length - 1]; - const isEndDelimited = - typeof endToken === "string" - ? delimiter.indexOf(endToken[endToken.length - 1]) > -1 - : !endToken; - - if (!strict) { - route += `(?:${delimiterRe}${loose ? "+" : ""}(?=${endsWithRe}))?`; - } + if (trailing) pattern += `${delimiterRe}${loose ? "*" : "?"}`; + pattern += end ? "$" : `(?=${delimiterRe}|$)`; - if (!isEndDelimited) { - route += `(?=${delimiterRe}|${endsWithRe})`; - } - } - - return new RegExp(route, flags(options)); + return new RegExp(pattern, flags(options)); } /** @@ -665,8 +647,8 @@ export type Path = string | RegExp | Array; */ export function pathToRegexp( path: Path, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, + keys: Key[] = [], + options: TokensToRegexpOptions & ParseOptions = {}, ) { if (path instanceof RegExp) return regexpToRegexp(path, keys); if (Array.isArray(path)) return arrayToRegexp(path, keys, options); diff --git a/tsconfig.json b/tsconfig.json index 03b0dd0..83a86d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "@borderless/ts-scripts/configs/tsconfig.json", "compilerOptions": { - "target": "ES2015", - "lib": ["ES2015"], + "target": "ES2020", + "lib": ["ES2020"], "rootDir": "src", "outDir": "dist", "module": "NodeNext", From f07b28931dbd90796b683ef2e0ca2181c16835c9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 22 May 2024 20:28:49 -0700 Subject: [PATCH 12/55] Rewrite compiler to fix edge case characters --- package.json | 2 +- src/index.spec.ts | 263 +++++++++++----------- src/index.ts | 550 ++++++++++++++++++++++++++-------------------- 3 files changed, 452 insertions(+), 363 deletions(-) diff --git a/package.json b/package.json index d914b63..ed066c7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "size-limit": [ { "path": "dist/index.js", - "limit": "2.1 kB" + "limit": "2.2 kB" } ], "ts-scripts": { diff --git a/src/index.spec.ts b/src/index.spec.ts index d9673f8..b0bffac 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -4,17 +4,10 @@ import * as pathToRegexp from "./index"; type Test = [ pathToRegexp.Path, - (pathToRegexp.TokensToRegexpOptions & pathToRegexp.ParseOptions) | undefined, + pathToRegexp.MatchOptions | undefined, pathToRegexp.Token[], - Array< - [ - string, - (string | undefined)[] | null, - pathToRegexp.Match?, - pathToRegexp.RegexpToFunctionOptions?, - ] - >, - Array<[any, string | null, pathToRegexp.TokensToFunctionOptions?]>, + Array<[string, (string | undefined)[] | null, pathToRegexp.Match?]>, + Array<[any, string | null, pathToRegexp.CompileOptions?]>, ]; /** @@ -33,7 +26,7 @@ const TESTS: Test[] = [ ["/route", null, false], ], [ - [null, "/"], + [undefined, "/"], [{}, "/"], [{ id: 123 }, "/"], ], @@ -49,7 +42,7 @@ const TESTS: Test[] = [ ["/test/", ["/test/"], { path: "/test/", index: 0, params: {} }], ], [ - [null, "/test"], + [undefined, "/test"], [{}, "/test"], ], ], @@ -62,7 +55,7 @@ const TESTS: Test[] = [ ["/test/", ["/test/"]], ["/test//", ["/test//"]], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], /** @@ -78,7 +71,7 @@ const TESTS: Test[] = [ ["/test", ["/test"]], ["/TEST", null], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/TEST", @@ -90,7 +83,7 @@ const TESTS: Test[] = [ ["/test", null], ["/TEST", ["/TEST"]], ], - [[null, "/TEST"]], + [[undefined, "/TEST"]], ], /** @@ -107,7 +100,7 @@ const TESTS: Test[] = [ ["/test/", null], ["/TEST", ["/TEST"]], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/test/", @@ -121,7 +114,7 @@ const TESTS: Test[] = [ ["/test//", ["/test//"]], ["/test/route", null], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], /** @@ -139,7 +132,7 @@ const TESTS: Test[] = [ ["/test/route", ["/test"]], ["/route", null], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/test/", @@ -154,7 +147,7 @@ const TESTS: Test[] = [ ["/test//", ["/test//"]], ["/foo//bar", null], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], [ "/:test", @@ -176,22 +169,16 @@ const TESTS: Test[] = [ ["/route", "route"], { path: "/route", index: 0, params: { test: "route" } }, ], - [ - "/caf%C3%A9", - ["/caf%C3%A9", "caf%C3%A9"], - { path: "/caf%C3%A9", index: 0, params: { test: "caf%C3%A9" } }, - ], [ "/caf%C3%A9", ["/caf%C3%A9", "caf%C3%A9"], { path: "/caf%C3%A9", index: 0, params: { test: "café" } }, - { decode: decodeURIComponent }, ], ], [ [{}, null], [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b"], + [{ test: "a+b" }, "/a+b", { encode: (x) => x }], [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], @@ -230,7 +217,7 @@ const TESTS: Test[] = [ ["/route", [""]], ["/route/", [""]], ], - [[null, ""]], + [[undefined, ""]], ], /** @@ -250,7 +237,7 @@ const TESTS: Test[] = [ ["/route/test/deep", null], ["/route", null], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/test/", @@ -265,7 +252,7 @@ const TESTS: Test[] = [ ["/test//", ["/test//"]], ["/route/test/", ["/test/"]], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], [ "/:test", @@ -285,7 +272,7 @@ const TESTS: Test[] = [ [ [{}, null], [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b"], + [{ test: "a+b" }, "/a+b", { encode: (x) => x }], [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], @@ -324,7 +311,7 @@ const TESTS: Test[] = [ ["/route", [""]], ["/route/", ["/"]], ], - [[null, ""]], + [[undefined, ""]], ], /** @@ -342,7 +329,7 @@ const TESTS: Test[] = [ ["/test/", ["/test"]], ["/test/route", ["/test"]], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/test/", @@ -358,7 +345,7 @@ const TESTS: Test[] = [ ["/test/route", null], ["/test//route", ["/test/"]], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], [ "/test.json", @@ -372,7 +359,7 @@ const TESTS: Test[] = [ ["/test.json.hbs", null], ["/test.json/route", ["/test.json"]], ], - [[null, "/test.json"]], + [[undefined, "/test.json"]], ], [ "/:test", @@ -433,7 +420,7 @@ const TESTS: Test[] = [ ["/test/route", ["/test"]], ["/route/test/deep", ["/test"]], ], - [[null, "/test"]], + [[undefined, "/test"]], ], [ "/test/", @@ -449,7 +436,7 @@ const TESTS: Test[] = [ ["/test/route", null], ["/route/test/deep", null], ], - [[null, "/test/"]], + [[undefined, "/test/"]], ], [ "/test.json", @@ -464,7 +451,7 @@ const TESTS: Test[] = [ ["/test.json/route", ["/test.json"]], ["/route/test.json/deep", ["/test.json"]], ], - [[null, "/test.json"]], + [[undefined, "/test.json"]], ], [ "/:test", @@ -539,7 +526,7 @@ const TESTS: Test[] = [ }, ["/test"], [["/test/route", ["/test"]]], - [[null, "/test"]], + [[undefined, "/test"]], ], /** @@ -673,7 +660,7 @@ const TESTS: Test[] = [ ["//", ["//", undefined], { path: "//", index: 0, params: {} }], ], [ - [null, ""], + [undefined, ""], [{ test: "foobar" }, "/foobar"], ], ], @@ -697,7 +684,7 @@ const TESTS: Test[] = [ ["//", null], ], [ - [null, ""], + [undefined, ""], [{ test: "foobar" }, "/foobar"], ], ], @@ -723,7 +710,7 @@ const TESTS: Test[] = [ ["//", ["//", undefined]], ], [ - [null, "/"], + [undefined, "/"], [{ test: "foobar" }, "/foobar/"], ], ], @@ -745,7 +732,7 @@ const TESTS: Test[] = [ ["/foo/bar", ["/foo/bar", "foo"]], ], [ - [null, "/bar"], + [undefined, "/bar"], [{ test: "foo" }, "/foo/bar"], ], ], @@ -782,6 +769,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "[^\\/]+?", + separator: "/", }, "-bar", ], @@ -791,7 +779,11 @@ const TESTS: Test[] = [ ["/foo-bar", ["/foo-bar", "foo"]], ["/foo/baz-bar", ["/foo/baz-bar", "foo/baz"]], ], - [[{ test: "foo" }, "/foo-bar"]], + [ + [{}, "-bar"], + [{ test: [] }, "-bar"], + [{ test: ["foo"] }, "/foo-bar"], + ], ], /** @@ -807,6 +799,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "[^\\/]+?", + separator: "/", }, ], [ @@ -829,7 +822,7 @@ const TESTS: Test[] = [ ], [ [{}, null], - [{ test: "foobar" }, "/foobar"], + [{ test: ["foobar"] }, "/foobar"], [{ test: ["a", "b", "c"] }, "/a/b/c"], ], ], @@ -843,6 +836,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "\\d+", + separator: "/", }, ], [ @@ -850,9 +844,9 @@ const TESTS: Test[] = [ ["/123/456/789", ["/123/456/789", "123/456/789"]], ], [ - [{ test: "abc" }, null], - [{ test: 123 }, "/123"], - [{ test: [1, 2, 3] }, "/1/2/3"], + [{ test: ["abc"] }, null], + [{ test: ["123"] }, "/123"], + [{ test: ["1", "2", "3"] }, "/1/2/3"], ], ], [ @@ -866,6 +860,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "json|xml", + separator: ".", }, ], [ @@ -875,8 +870,8 @@ const TESTS: Test[] = [ ["/route.html", null], ], [ - [{ ext: "foobar" }, null], - [{ ext: "xml" }, "/route.xml"], + [{ ext: ["foobar"] }, null], + [{ ext: ["xml"] }, "/route.xml"], [{ ext: ["xml", "json"] }, "/route.xml.json"], ], ], @@ -916,6 +911,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "[^\\/]+?", + separator: "/", }, ], [ @@ -939,7 +935,7 @@ const TESTS: Test[] = [ [ [{}, ""], [{ test: [] }, ""], - [{ test: "foobar" }, "/foobar"], + [{ test: ["foobar"] }, "/foobar"], [{ test: ["foo", "bar"] }, "/foo/bar"], ], ], @@ -954,6 +950,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "[a-z]+", + separator: ".", }, ], [ @@ -965,8 +962,8 @@ const TESTS: Test[] = [ [ [{}, "/route"], [{ ext: [] }, "/route"], - [{ ext: "123" }, null], - [{ ext: "foobar" }, "/route.foobar"], + [{ ext: ["123"] }, null], + [{ ext: ["foobar"] }, "/route.foobar"], [{ ext: ["foo", "bar"] }, "/route.foo.bar"], ], ], @@ -1037,12 +1034,8 @@ const TESTS: Test[] = [ [ [{ test: "" }, "/"], [{ test: "abc" }, "/abc"], - [{ test: "abc/123" }, "/abc%2F123", { encode: encodeURIComponent }], - [ - { test: "abc/123/456" }, - "/abc%2F123%2F456", - { encode: encodeURIComponent }, - ], + [{ test: "abc/123" }, "/abc%2F123"], + [{ test: "abc/123/456" }, "/abc%2F123%2F456"], ], ], [ @@ -1103,6 +1096,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "abc|xyz", + separator: "/", }, ], [ @@ -1114,13 +1108,13 @@ const TESTS: Test[] = [ ["/xyzxyz", null], ], [ - [{ path: "abc" }, "/abc"], + [{ path: ["abc"] }, "/abc"], [{ path: ["abc", "xyz"] }, "/abc/xyz"], [{ path: ["xyz", "abc", "xyz"] }, "/xyz/abc/xyz"], - [{ path: "abc123" }, null], - [{ path: "abc123" }, "/abc123", { validate: false }], - [{ path: "abcxyz" }, null], - [{ path: "abcxyz" }, "/abcxyz", { validate: false }], + [{ path: ["abc123"] }, null], + [{ path: ["abc123"] }, "/abc123", { validate: false }], + [{ path: ["abcxyz"] }, null], + [{ path: ["abcxyz"] }, "/abcxyz", { validate: false }], ], ], @@ -1135,7 +1129,7 @@ const TESTS: Test[] = [ ["test", ["test"]], ["/test", null], ], - [[null, "test"]], + [[undefined, "test"]], ], [ ":test", @@ -1224,7 +1218,7 @@ const TESTS: Test[] = [ ], [ [{}, ""], - [{ test: "" }, null], + [{ test: "" }, ""], [{ test: "route" }, "route"], ], ], @@ -1238,6 +1232,7 @@ const TESTS: Test[] = [ suffix: "/", modifier: "+", pattern: "[^\\/]+?", + separator: "/", }, ], [ @@ -1358,6 +1353,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "[^\\/]+?", + separator: ".", }, ], [ @@ -1366,7 +1362,7 @@ const TESTS: Test[] = [ ], [ [{ format: [] }, null], - [{ format: "foo" }, "/test.foo"], + [{ format: ["foo"] }, "/test.foo"], [{ format: ["foo", "bar"] }, "/test.foo.bar"], ], ], @@ -1643,7 +1639,7 @@ const TESTS: Test[] = [ ")", ], [["/route(\\123\\)", ["/route(\\123\\)", "123\\"]]], - [[["123\\"], "/route(\\123\\)"]], + [[["123\\"], "/route(\\123\\)", { encode: (x) => x }]], ], [ "{/login}?", @@ -1662,7 +1658,7 @@ const TESTS: Test[] = [ ["/login", ["/login"]], ], [ - [null, ""], + [undefined, ""], [{ "": "" }, "/login"], ], ], @@ -1715,6 +1711,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "[^\\/]+?", + separator: "/", }, ], [ @@ -1724,7 +1721,7 @@ const TESTS: Test[] = [ ], [ [{ 0: null }, ""], - [{ 0: "x" }, "/x"], + [{ 0: ["x"] }, "/x"], [{ 0: ["a", "b", "c"] }, "/a/b/c"], ], ], @@ -1738,6 +1735,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "[^\\/]+?", + separator: "/", }, ], [ @@ -1748,8 +1746,8 @@ const TESTS: Test[] = [ ], [ [{ 0: "" }, null], - [{ 0: "x" }, "/x"], - [{ 0: "route" }, "/route"], + [{ 0: ["x"] }, "/x"], + [{ 0: ["route"] }, "/route"], [{ 0: ["a", "b", "c"] }, "/a/b/c"], ], ], @@ -1934,14 +1932,14 @@ const TESTS: Test[] = [ ["/testing", null], ["/(testing)", ["/(testing)"]], ], - [[null, "/(testing)"]], + [[undefined, "/(testing)"]], ], [ "/.\\+\\*\\?\\{\\}=^!\\:$[]|", undefined, ["/.+*?{}=^!:$[]|"], [["/.+*?{}=^!:$[]|", ["/.+*?{}=^!:$[]|"]]], - [[null, "/.+*?{}=^!:$[]|"]], + [[undefined, "/.+*?{}=^!:$[]|"]], ], [ "/test\\/:uid(u\\d+)?:cid(c\\d+)?", @@ -2107,6 +2105,7 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "[^\\/]+?", + separator: "/", }, "baz", ], @@ -2116,8 +2115,8 @@ const TESTS: Test[] = [ ["/baz", null], ], [ - [{ foo: "foo" }, "/foobaz"], - [{ foo: "foo/bar" }, "/foo%2Fbarbaz", { encode: encodeURIComponent }], + [{ foo: [] }, null], + [{ foo: ["foo"] }, "/foobaz"], [{ foo: ["foo", "bar"] }, "/foo/barbaz"], ], ], @@ -2282,17 +2281,23 @@ const TESTS: Test[] = [ ], [["/café", ["/café", "café"]]], [ - [{ foo: "café" }, "/café"], - [{ foo: "café" }, "/caf%C3%A9", { encode: encodeURIComponent }], + [{ foo: "café" }, "/café", { encode: (x) => x }], + [{ foo: "café" }, "/caf%C3%A9"], ], ], - ["/café", undefined, ["/café"], [["/café", ["/café"]]], [[null, "/café"]]], [ "/café", - { encode: encodeURIComponent }, + undefined, + ["/café"], + [["/café", ["/café"]]], + [[undefined, "/café"]], + ], + [ + "/café", + { encodePath: encodeURI }, ["/caf%C3%A9"], [["/caf%C3%A9", ["/caf%C3%A9"]]], - [[null, "/caf%C3%A9"]], + [[undefined, "/caf%C3%A9"]], ], [ "packages/", @@ -2302,7 +2307,7 @@ const TESTS: Test[] = [ ["packages", null], ["packages/", ["packages/"]], ], - [[null, "packages/"]], + [[undefined, "packages/"]], ], /** @@ -2390,7 +2395,7 @@ const TESTS: Test[] = [ ["this is a test", ["this is"]], ["this isn't", null], ], - [[null, "this is"]], + [[undefined, "this is"]], ], /** @@ -2537,8 +2542,6 @@ const TESTS: Test[] = [ ["/123.abc", null], ], [ - [{ test: 123 }, "/123"], - [{ test: 123.123 }, "/123.123"], [{ test: "abc" }, null], [{ test: "123" }, "/123"], [{ test: "123.123" }, "/123.123"], @@ -2610,10 +2613,17 @@ const TESTS: Test[] = [ suffix: "", modifier: "*", pattern: "[^\\/]+?", + separator: "/", }, ], - [["foobar", ["foobar", "foobar"]]], - [[{ name: "foobar" }, "foobar"]], + [ + ["foobar", ["foobar", "foobar"]], + ["foo/bar", ["foo/bar", "foo/bar"]], + ], + [ + [{ name: ["foobar"] }, "foobar"], + [{ name: ["foo", "bar"] }, "foo/bar"], + ], ], [ ":name+", @@ -2625,10 +2635,11 @@ const TESTS: Test[] = [ suffix: "", modifier: "+", pattern: "[^\\/]+?", + separator: "/", }, ], [["foobar", ["foobar", "foobar"]]], - [[{ name: "foobar" }, "foobar"]], + [[{ name: ["foobar"] }, "foobar"]], ], /** @@ -2721,16 +2732,6 @@ const TESTS: Test[] = [ * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { - const TEST_PATH = "/user/:id"; - - const TEST_PARAM = { - name: "id", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }; - describe("arguments", () => { it("should work without different call combinations", () => { pathToRegexp.pathToRegexp("/test"); @@ -2748,9 +2749,19 @@ describe("path-to-regexp", () => { it("should accept an array of keys as the second argument", () => { const keys: pathToRegexp.Key[] = []; - const re = pathToRegexp.pathToRegexp(TEST_PATH, keys, { end: false }); + const re = pathToRegexp.pathToRegexp("/user/:id", keys, { end: false }); + + const expectedKeys = [ + { + name: "id", + prefix: "/", + suffix: "", + modifier: "", + pattern: "[^\\/]+?", + }, + ]; - expect(keys).toEqual([TEST_PARAM]); + expect(keys).toEqual(expectedKeys); expect(exec(re, "/user/123/show")).toEqual(["/user/123", "123"]); }); @@ -2791,22 +2802,6 @@ describe("path-to-regexp", () => { }); }); - describe("tokens", () => { - const tokens = pathToRegexp.parse(TEST_PATH); - - it("should expose method to compile tokens to regexp", () => { - const re = pathToRegexp.tokensToRegexp(tokens); - - expect(exec(re, "/user/123")).toEqual(["/user/123", "123"]); - }); - - it("should expose method to compile tokens to a path function", () => { - const fn = pathToRegexp.tokensToFunction(tokens); - - expect(fn({ id: 123 })).toEqual("/user/123"); - }); - }); - describe("rules", () => { TESTS.forEach(([path, opts, tokens, matchCases, compileCases]) => { describe(util.inspect(path), () => { @@ -2816,7 +2811,7 @@ describe("path-to-regexp", () => { // Parsing and compiling is only supported with string input. if (typeof path === "string") { it("should parse", () => { - expect(pathToRegexp.parse(path, opts)).toEqual(tokens); + expect(pathToRegexp.parse(path, opts).tokens).toEqual(tokens); }); describe("compile", () => { @@ -2848,7 +2843,7 @@ describe("path-to-regexp", () => { } describe("match" + (opts ? " using " + util.inspect(opts) : ""), () => { - matchCases.forEach(([pathname, matches, params, options]) => { + matchCases.forEach(([pathname, matches, params]) => { const message = `should ${ matches ? "" : "not " }match ${util.inspect(pathname)}`; @@ -2858,7 +2853,7 @@ describe("path-to-regexp", () => { }); if (typeof path === "string" && params !== undefined) { - const match = pathToRegexp.match(path, options); + const match = pathToRegexp.match(path, opts); it(message + " params", () => { expect(match(pathname)).toEqual(params); @@ -2884,9 +2879,7 @@ describe("path-to-regexp", () => { expect(() => { toPath({ foo: "abc" }); - }).toThrow( - new TypeError('Expected "foo" to match "\\d+", but got "abc"'), - ); + }).toThrow(new TypeError('Invalid value for "foo": "/abc"')); }); it("should throw when expecting a repeated value", () => { @@ -2894,7 +2887,7 @@ describe("path-to-regexp", () => { expect(() => { toPath({ foo: [] }); - }).toThrow(new TypeError('Expected "foo" to not be empty')); + }).toThrow(new TypeError('Invalid value for "foo": ""')); }); it("should throw when not expecting a repeated value", () => { @@ -2902,19 +2895,31 @@ describe("path-to-regexp", () => { expect(() => { toPath({ foo: [] }); - }).toThrow( - new TypeError('Expected "foo" to not repeat, but got an array'), - ); + }).toThrow(new TypeError('Expected "foo" to be a string')); + }); + + it("should throw when a repeated param is not an array", () => { + const toPath = pathToRegexp.compile("/:foo+"); + + expect(() => { + toPath({ foo: "a" }); + }).toThrow(new TypeError('Expected "foo" to be an array')); + }); + + it("should throw when an array value is not a string", () => { + const toPath = pathToRegexp.compile("/:foo+"); + + expect(() => { + toPath({ foo: [1, "a"] }); + }).toThrow(new TypeError('Expected "foo/0" to be a string')); }); it("should throw when repeated value does not match", () => { const toPath = pathToRegexp.compile("/:foo(\\d+)+"); expect(() => { - toPath({ foo: [1, 2, 3, "a"] }); - }).toThrow( - new TypeError('Expected all "foo" to match "\\d+", but got "a"'), - ); + toPath({ foo: ["1", "2", "3", "a"] }); + }).toThrow(new TypeError('Invalid value for "foo": "/1/2/3/a"')); }); }); }); diff --git a/src/index.ts b/src/index.ts index 28f7357..b75b973 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,81 @@ const DEFAULT_PREFIXES = "./"; const DEFAULT_DELIMITER = "/"; -const NOOP_ENCODE = (x: string) => x; -const NOOP_DECODE = (x: string) => x; - const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; +const NOOP_VALUE = (value: string) => value; + +/** + * Encode a string into another string. + */ +export type Encode = (value: string) => string; + +/** + * Decode a string into another string. + */ +export type Decode = (value: string) => string; + +export interface ParseOptions { + /** + * Set the default delimiter for repeat parameters. (default: `'/'`) + */ + delimiter?: string; + /** + * List of characters to automatically consider prefixes when parsing. + */ + prefixes?: string; + /** + * Function for encoding input strings for output into path. + */ + encodePath?: Encode; +} + +export interface PathToRegexpOptions extends ParseOptions { + /** + * When `true` the regexp will be case sensitive. (default: `false`) + */ + sensitive?: boolean; + /** + * Set characters to treat as "loose" and allow arbitrarily repeated. (default: `/`) + */ + loose?: string; + /** + * When `true` the regexp will match to the end of the string. (default: `true`) + */ + end?: boolean; + /** + * When `true` the regexp will match from the beginning of the string. (default: `true`) + */ + start?: boolean; + /** + * When `true` the regexp allows an optional trailing delimiter to match. (default: `true`) + */ + trailing?: boolean; +} + +export interface MatchOptions extends PathToRegexpOptions { + /** + * Function for decoding strings for params. + */ + decode?: Decode; +} + +export interface CompileOptions extends ParseOptions { + /** + * When `true` the validation will be case sensitive. (default: `false`) + */ + sensitive?: boolean; + /** + * Set characters to treat as "loose" and allow arbitrarily repeated. (default: `/`) + */ + loose?: string; + /** + * When `false` the function can produce an invalid (unmatched) path. (default: `true`) + */ + validate?: boolean; + /** + * Function for encoding input strings for output into the path. (default: `encodeURIComponent`) + */ + encode?: Encode; +} /** * Tokenizer results. @@ -165,42 +237,36 @@ class Iter { } } -export interface ParseOptions { - /** - * Set the default delimiter for repeat parameters. (default: `'/'`) - */ - delimiter?: string; - /** - * List of characters to automatically consider prefixes when parsing. - */ - prefixes?: string; - /** - * Function for encoding input strings for output into path. - */ - encode?: Encode; +/** + * Tokenized path instance. Can we passed around instead of string. + */ +export class TokenData { + constructor( + public readonly tokens: Token[], + public readonly delimiter: string, + ) {} } /** * Parse a string for the raw tokens. */ -export function parse(str: string, options: ParseOptions = {}): Token[] { +export function parse(str: string, options: ParseOptions = {}): TokenData { const { prefixes = DEFAULT_PREFIXES, delimiter = DEFAULT_DELIMITER, - encode = NOOP_ENCODE, + encodePath = NOOP_VALUE, } = options; const defaultPattern = `[^${escape(delimiter)}]+?`; - const result: Token[] = []; - const tokens = lexer(str); - const stringify = encoder(delimiter, encode, NOOP_ENCODE); + const tokens: Token[] = []; + const iter = lexer(str); let key = 0; let path = ""; do { - const char = tokens.tryConsume("CHAR"); - const name = tokens.tryConsume("NAME"); - const pattern = tokens.tryConsume("PATTERN"); - const modifier = tokens.tryConsume("MODIFIER"); + const char = iter.tryConsume("CHAR"); + const name = iter.tryConsume("NAME"); + const pattern = iter.tryConsume("PATTERN"); + const modifier = iter.tryConsume("MODIFIER"); if (name || pattern || modifier) { let prefix = char || ""; @@ -211,165 +277,208 @@ export function parse(str: string, options: ParseOptions = {}): Token[] { } if (path) { - result.push(stringify(path)); + tokens.push(encodePath(path)); path = ""; } - result.push({ - name: name || key++, - prefix: stringify(prefix), - suffix: "", - pattern: pattern || defaultPattern, - modifier: modifier || "", - }); + tokens.push( + toKey( + encodePath, + delimiter, + name || key++, + pattern || defaultPattern, + prefix, + "", + modifier, + ), + ); continue; } - const value = char || tokens.tryConsume("ESCAPED_CHAR"); + const value = char || iter.tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; } if (path) { - result.push(stringify(path)); + tokens.push(encodePath(path)); path = ""; } - const open = tokens.tryConsume("OPEN"); + const open = iter.tryConsume("OPEN"); if (open) { - const prefix = tokens.text(); - const name = tokens.tryConsume("NAME") || ""; - const pattern = tokens.tryConsume("PATTERN") || ""; - const suffix = tokens.text(); - - tokens.consume("CLOSE"); - - result.push({ - name: name || (pattern ? key++ : ""), - pattern: name && !pattern ? defaultPattern : pattern, - prefix: stringify(prefix), - suffix: stringify(suffix), - modifier: tokens.tryConsume("MODIFIER") || "", - }); + const prefix = iter.text(); + const name = iter.tryConsume("NAME"); + const pattern = iter.tryConsume("PATTERN"); + const suffix = iter.text(); + + iter.consume("CLOSE"); + + const modifier = iter.tryConsume("MODIFIER"); + + // TODO: Create non-matching version of keys to switch on/off in `compile`. + // TODO: Make optional trailing `/` a version of this so the info is in the "token". + tokens.push( + toKey( + encodePath, + delimiter, + name || (pattern ? key++ : ""), + name && !pattern ? defaultPattern : pattern || "", + prefix, + suffix, + modifier, + ), + ); continue; } - tokens.consume("END"); + iter.consume("END"); break; } while (true); - return result; + return new TokenData(tokens, delimiter); } -export type Encode = (value: string) => string; - -export interface TokensToFunctionOptions { - /** - * When `true` the regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * When `false` the function can produce an invalid (unmatched) path. (default: `true`) - */ - validate?: boolean; - /** - * Function for encoding input strings for output. - */ - encode?: Encode; +function toKey( + encode: Encode, + delimiter: string, + name: string | number, + pattern = "", + inputPrefix = "", + inputSuffix = "", + modifier = "", +): Key { + const prefix = encode(inputPrefix); + const suffix = encode(inputSuffix); + const separator = + modifier === "*" || modifier === "+" + ? prefix + suffix || delimiter + : undefined; + return { name, prefix, suffix, pattern, modifier, separator }; } /** * Compile a string to a template function for the path. */ export function compile

( - value: string, - options?: ParseOptions & TokensToFunctionOptions, + value: string | TokenData, + options: CompileOptions = {}, ) { - return tokensToFunction

(parse(value, options), options); + const data = value instanceof TokenData ? value : parse(value, options); + return compileTokens

(data, options); } -export type PathFunction

= (data?: P) => string; +export type ParamData = Partial>; +export type PathFunction

= (data?: P) => string; /** - * Transform tokens into a path building function. + * Convert a single token into a path building function. */ -export function tokensToFunction

( - tokens: Token[], - options: TokensToFunctionOptions = {}, -): PathFunction

{ - const reFlags = flags(options); - const { encode = NOOP_ENCODE, validate = true } = options; - - // Compile all the tokens into regexps. - const matches = tokens.map((token) => { - if (typeof token === "object") { - return new RegExp(`^(?:${token.pattern})$`, reFlags); - } - }); +function tokenToFunction( + token: Token, + encode: Encode, +): (data: ParamData) => string { + if (typeof token === "string") { + return () => token; + } - return function path(data: Record | undefined) { - let path = ""; + const optional = token.modifier === "?" || token.modifier === "*"; - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; + if (token.separator) { + const stringify = (value: string, index: number) => { + if (typeof value !== "string") { + throw new TypeError(`Expected "${token.name}/${index}" to be a string`); + } + return encode(value); + }; - if (typeof token === "string") { - path += token; - continue; + const compile = (value: unknown) => { + if (!Array.isArray(value)) { + throw new TypeError(`Expected "${token.name}" to be an array`); } - const value = data ? data[token.name] : undefined; - const optional = token.modifier === "?" || token.modifier === "*"; - const repeat = token.modifier === "*" || token.modifier === "+"; + if (value.length === 0) return ""; - if (Array.isArray(value)) { - if (!repeat) { - throw new TypeError( - `Expected "${token.name}" to not repeat, but got an array`, - ); - } + return ( + token.prefix + value.map(stringify).join(token.separator) + token.suffix + ); + }; - if (value.length === 0) { - if (optional) continue; + if (optional) { + return (data): string => { + const value = data[token.name]; + if (value == null) return ""; + return value.length ? compile(value) : ""; + }; + } - throw new TypeError(`Expected "${token.name}" to not be empty`); - } + return (data): string => { + const value = data[token.name]; + return compile(value); + }; + } - for (let j = 0; j < value.length; j++) { - const segment = encode(value[j]); + const stringify = (value: unknown) => { + if (typeof value !== "string") { + throw new TypeError(`Expected "${token.name}" to be a string`); + } + return token.prefix + encode(value) + token.suffix; + }; - if (validate && !(matches[i] as RegExp).test(segment)) { - throw new TypeError( - `Expected all "${token.name}" to match "${token.pattern}", but got "${segment}"`, - ); - } + if (optional) { + return (data): string => { + const value = data[token.name]; + if (value == null) return ""; + return stringify(value); + }; + } - path += token.prefix + segment + token.suffix; - } + return (data): string => { + const value = data[token.name]; + return stringify(value); + }; +} - continue; - } +/** + * Transform tokens into a path building function. + */ +function compileTokens

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

{ + const { + encode = encodeURIComponent, + validate = true, + loose = DEFAULT_DELIMITER, + } = options; + const reFlags = flags(options); + const stringify = toStringify(loose); + + // Compile all the tokens into regexps. + const encoders: Array<(data: ParamData) => string> = data.tokens.map( + (token) => { + const fn = tokenToFunction(token, encode); + if (!validate || typeof token === "string") return fn; - if (typeof value === "string" || typeof value === "number") { - const segment = encode(String(value)); + const pattern = keyToRegexp(token, stringify); + const validRe = new RegExp(`^${pattern}$`, reFlags); - if (validate && !(matches[i] as RegExp).test(segment)) { + return (data) => { + const value = fn(data); + if (!validRe.test(value)) { throw new TypeError( - `Expected "${token.name}" to match "${token.pattern}", but got "${segment}"`, + `Invalid value for "${token.name}": ${JSON.stringify(value)}`, ); } + return value; + }; + }, + ); - path += token.prefix + segment + token.suffix; - continue; - } - - if (optional) continue; - - const typeOfMessage = repeat ? "an array" : "a string"; - throw new TypeError(`Expected "${token.name}" to be ${typeOfMessage}`); - } - + return function path(data: Record = {}) { + let path = ""; + for (const encoder of encoders) path += encoder(data); return path; }; } @@ -377,7 +486,7 @@ export function tokensToFunction

( /** * A match result contains data about the path match. */ -export interface MatchResult

{ +export interface MatchResult

{ path: string; index: number; params: P; @@ -386,46 +495,42 @@ export interface MatchResult

{ /** * A match is either `false` (no match) or a match result. */ -export type Match

= false | MatchResult

; +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

; /** * Create path match function from `path-to-regexp` spec. */ -export function match

( +export function match

( str: Path, - options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions, -) { + options: MatchOptions = {}, +): MatchFunction

{ const keys: Key[] = []; const re = pathToRegexp(str, keys, options); - return regexpToFunction

(re, keys, options); -} - -export interface RegexpToFunctionOptions { - /** - * Function for decoding strings for params. - */ - decode?: (value: string) => string; + return matchRegexp

(re, keys, options); } /** * Create a path match function from `path-to-regexp` output. */ -export function regexpToFunction

( +function matchRegexp

( re: RegExp, keys: Key[], - options: RegexpToFunctionOptions = {}, + options: MatchOptions, ): MatchFunction

{ - const { decode = NOOP_DECODE } = options; + const { decode = decodeURIComponent, loose = DEFAULT_DELIMITER } = options; + const stringify = toStringify(loose); const decoders = keys.map((key) => { - if (key.split) { - const re = new RegExp(`(${key.pattern})(?:${key.split}|$)`, "g"); + if (key.separator) { + const re = new RegExp( + `(${key.pattern})(?:${stringify(key.separator)}|$)`, + "g", + ); + return (value: string) => { const result: string[] = []; for (const m of value.matchAll(re)) result.push(decode(m[1])); @@ -472,15 +577,15 @@ function repeat(str: string) { /** * Encode all non-delimiter characters using the encode function. */ -function encoder( - delimiter: string, - encodeString: Encode, - encodeDelimiter: Encode, -) { - const re = new RegExp(`[^${escape(delimiter)}]+|(.)`, "g"); - const replacer = (value: string, delimiter: string) => - delimiter ? encodeDelimiter(value) : encodeString(value); - return (value: string) => value.replace(re, replacer); +function toStringify(loose: string) { + if (loose) { + const re = new RegExp(`[^${escape(loose)}]+|(.)`, "g"); + const replacer = (value: string, loose: string) => + loose ? repeat(value) : escape(value); + return (value: string) => value.replace(re, replacer); + } + + return escape; } /** @@ -490,28 +595,22 @@ function flags(options: { sensitive?: boolean }) { return options.sensitive ? "" : "i"; } -export interface TokenKey { +/** + * A key is a capture group in the regex. + */ +export interface Key { name: string | number; prefix: string; suffix: string; pattern: string; modifier: string; + separator?: string; } /** * A token is a string (nothing special) or key metadata (capture group). */ -export type Token = string | TokenKey; - -/** - * Metadata about a key. - */ -export interface Key extends TokenKey { - /** - * Internal flag indicating the key needs to be split for the match. - */ - split?: string; -} +export type Token = string | Key; /** * Pull out keys from a regexp. @@ -538,9 +637,9 @@ function regexpToRegexp(path: RegExp, keys: Key[]): RegExp { * Transform an array into a regexp. */ function arrayToRegexp( - paths: Array, + paths: PathItem[], keys: Key[], - options: TokensToRegexpOptions & ParseOptions, + options: PathToRegexpOptions, ): RegExp { const parts = paths.map((path) => pathToRegexp(path, keys, options).source); return new RegExp(`(?:${parts.join("|")})`, flags(options)); @@ -552,91 +651,75 @@ function arrayToRegexp( function stringToRegexp( path: string, keys: Key[], - options: TokensToRegexpOptions & ParseOptions, + options: PathToRegexpOptions, ) { return tokensToRegexp(parse(path, options), keys, options); } -export interface TokensToRegexpOptions { - /** - * When `true` the regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * When `true` the regexp allows an optional trailing delimiter to match. (default: `true`) - */ - trailing?: boolean; - /** - * When `true` all delimiters can be repeated one or more times. (default: `true`) - */ - loose?: boolean; - /** - * When `true` the regexp will match to the end of the string. (default: `true`) - */ - end?: boolean; - /** - * When `true` the regexp will match from the beginning of the string. (default: `true`) - */ - start?: boolean; - /** - * Sets the final character for non-ending optimistic matches. (default: `"/"`) - */ - delimiter?: string; -} - /** * Expose a function for taking tokens and returning a RegExp. */ -export function tokensToRegexp( - tokens: Token[], - keys: Key[] = [], - options: TokensToRegexpOptions = {}, +function tokensToRegexp( + data: TokenData, + keys: Key[], + options: PathToRegexpOptions, ): RegExp { const { trailing = true, - loose = true, start = true, end = true, - delimiter = DEFAULT_DELIMITER, + loose = DEFAULT_DELIMITER, } = options; - const delimiterRe = escape(delimiter); - const stringify = loose ? encoder(delimiter, escape, repeat) : escape; + const stringify = toStringify(loose); let pattern = start ? "^" : ""; - // Iterate over the tokens and create our regexp string. - for (const token of tokens) { + for (const token of data.tokens) { if (typeof token === "string") { pattern += stringify(token); } else { - const prefix = stringify(token.prefix); - const suffix = stringify(token.suffix); - - if (token.pattern) { - if (token.modifier === "+" || token.modifier === "*") { - const mod = token.modifier === "*" ? "?" : ""; - const split = `${suffix}${prefix}` || delimiterRe; // Fallback to split on delimiter. - keys.push(Object.assign({}, token, { split })); - pattern += `(?:${prefix}((?:${token.pattern})(?:${split}(?:${token.pattern}))*)${suffix})${mod}`; - } else { - keys.push(token); - pattern += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; - } - } else { - pattern += `(?:${prefix}${suffix})${token.modifier}`; - } + if (token.pattern) keys.push(token); + pattern += keyToRegexp(token, stringify); } } - if (trailing) pattern += `${delimiterRe}${loose ? "*" : "?"}`; - pattern += end ? "$" : `(?=${delimiterRe}|$)`; + if (trailing) { + pattern += `(?:${stringify(data.delimiter)})${loose ? "?" : ""}`; + } + + pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; return new RegExp(pattern, flags(options)); } /** - * Supported `path-to-regexp` input types. + * Convert a token into a regexp string (re-used for path validation). + */ +function keyToRegexp(key: Key, stringify: Encode): string { + const prefix = stringify(key.prefix); + const suffix = stringify(key.suffix); + + if (key.pattern) { + if (key.separator) { + const mod = key.modifier === "*" ? "?" : ""; + const split = stringify(key.separator); + return `(?:${prefix}((?:${key.pattern})(?:${split}(?:${key.pattern}))*)${suffix})${mod}`; + } else { + return `(?:${prefix}(${key.pattern})${suffix})${key.modifier}`; + } + } else { + return `(?:${prefix}${suffix})${key.modifier}`; + } +} + +/** + * Simple input types. + */ +export type PathItem = string | RegExp | TokenData; + +/** + * Repeated and simple input types. */ -export type Path = string | RegExp | Array; +export type Path = PathItem | PathItem[]; /** * Normalize the given path string, returning a regular expression. @@ -648,8 +731,9 @@ export type Path = string | RegExp | Array; export function pathToRegexp( path: Path, keys: Key[] = [], - options: TokensToRegexpOptions & ParseOptions = {}, + options: PathToRegexpOptions = {}, ) { + if (path instanceof TokenData) return tokensToRegexp(path, keys, options); if (path instanceof RegExp) return regexpToRegexp(path, keys); if (Array.isArray(path)) return arrayToRegexp(path, keys, options); return stringToRegexp(path, keys, options); From 8a39eee334cd738088575cff84bbaaa639e3e40e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 22 May 2024 20:52:07 -0700 Subject: [PATCH 13/55] Allow unicode key names --- src/index.ts | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/index.ts b/src/index.ts index b75b973..19cd772 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ const DEFAULT_PREFIXES = "./"; const DEFAULT_DELIMITER = "/"; const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; const NOOP_VALUE = (value: string) => value; +const NAME_RE = /^[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]$/u; /** * Encode a string into another string. @@ -98,29 +99,30 @@ interface LexToken { * Tokenize input string. */ function lexer(str: string) { + const chars = [...str]; const tokens: LexToken[] = []; let i = 0; - while (i < str.length) { - const char = str[i]; + while (i < chars.length) { + const char = chars[i]; if (char === "*" || char === "+" || char === "?") { - tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); + tokens.push({ type: "MODIFIER", index: i, value: chars[i++] }); continue; } if (char === "\\") { - tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); + tokens.push({ type: "ESCAPED_CHAR", index: i++, value: chars[i++] }); continue; } if (char === "{") { - tokens.push({ type: "OPEN", index: i, value: str[i++] }); + tokens.push({ type: "OPEN", index: i, value: chars[i++] }); continue; } if (char === "}") { - tokens.push({ type: "CLOSE", index: i, value: str[i++] }); + tokens.push({ type: "CLOSE", index: i, value: chars[i++] }); continue; } @@ -128,20 +130,9 @@ function lexer(str: string) { let name = ""; let j = i + 1; - while (j < str.length) { - const code = str.charCodeAt(j); - - if ( - // `0-9` - (code >= 48 && code <= 57) || - // `A-Z` - (code >= 65 && code <= 90) || - // `a-z` - (code >= 97 && code <= 122) || - // `_` - code === 95 - ) { - name += str[j++]; + while (j < chars.length) { + if (NAME_RE.test(chars[j])) { + name += chars[j++]; continue; } @@ -160,30 +151,30 @@ function lexer(str: string) { let pattern = ""; let j = i + 1; - if (str[j] === "?") { + if (chars[j] === "?") { throw new TypeError(`Pattern cannot start with "?" at ${j}`); } - while (j < str.length) { - if (str[j] === "\\") { - pattern += str[j++] + str[j++]; + while (j < chars.length) { + if (chars[j] === "\\") { + pattern += chars[j++] + chars[j++]; continue; } - if (str[j] === ")") { + if (chars[j] === ")") { count--; if (count === 0) { j++; break; } - } else if (str[j] === "(") { + } else if (chars[j] === "(") { count++; - if (str[j + 1] !== "?") { + if (chars[j + 1] !== "?") { throw new TypeError(`Capturing groups are not allowed at ${j}`); } } - pattern += str[j++]; + pattern += chars[j++]; } if (count) throw new TypeError(`Unbalanced pattern at ${i}`); @@ -194,7 +185,7 @@ function lexer(str: string) { continue; } - tokens.push({ type: "CHAR", index: i, value: str[i++] }); + tokens.push({ type: "CHAR", index: i, value: chars[i++] }); } tokens.push({ type: "END", index: i, value: "" }); From 35a15d57677714b197af4ae8add44f137b834abe Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 22 May 2024 20:54:45 -0700 Subject: [PATCH 14/55] Simplify name loop during parse --- src/index.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 19cd772..0d5ee95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ const DEFAULT_PREFIXES = "./"; const DEFAULT_DELIMITER = "/"; const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; const NOOP_VALUE = (value: string) => value; -const NAME_RE = /^[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]$/u; +const NAME_RE = /^[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$]$/u; /** * Encode a string into another string. @@ -130,13 +130,8 @@ function lexer(str: string) { let name = ""; let j = i + 1; - while (j < chars.length) { - if (NAME_RE.test(chars[j])) { - name += chars[j++]; - continue; - } - - break; + while (NAME_RE.test(chars[j])) { + name += chars[j++]; } if (!name) throw new TypeError(`Missing parameter name at ${i}`); From afd140289efa91ee9d6d7adf1602e6e6acd7a60a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 22 May 2024 21:53:56 -0700 Subject: [PATCH 15/55] Reserve some characters for future use --- src/index.spec.ts | 2 +- src/index.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index b0bffac..b7a9228 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1935,7 +1935,7 @@ const TESTS: Test[] = [ [[undefined, "/(testing)"]], ], [ - "/.\\+\\*\\?\\{\\}=^!\\:$[]|", + "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", undefined, ["/.+*?{}=^!:$[]|"], [["/.+*?{}=^!:$[]|", ["/.+*?{}=^!:$[]|"]]], diff --git a/src/index.ts b/src/index.ts index 0d5ee95..d7517da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,6 +90,7 @@ interface LexToken { | "CHAR" | "ESCAPED_CHAR" | "MODIFIER" + | "RESERVED" | "END"; index: number; value: string; @@ -106,6 +107,11 @@ function lexer(str: string) { while (i < chars.length) { const char = chars[i]; + if (char === "!" || char === ";" || char === "|") { + tokens.push({ type: "RESERVED", index: i, value: chars[i++] }); + continue; + } + if (char === "*" || char === "+" || char === "?") { tokens.push({ type: "MODIFIER", index: i, value: chars[i++] }); continue; @@ -510,6 +516,7 @@ function matchRegexp

( ): MatchFunction

{ const { decode = decodeURIComponent, loose = DEFAULT_DELIMITER } = options; const stringify = toStringify(loose); + const decoders = keys.map((key) => { if (key.separator) { const re = new RegExp( From 425266ff89ade12a797cf95f1f0b8a69ffa2e76a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 23 May 2024 12:56:11 -0700 Subject: [PATCH 16/55] Default noop encode/decode, use ID start/continue --- src/index.spec.ts | 23 ++++++++++++++--------- src/index.ts | 48 +++++++++++++++++++++++------------------------ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index b7a9228..759f5f4 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -172,13 +172,13 @@ const TESTS: Test[] = [ [ "/caf%C3%A9", ["/caf%C3%A9", "caf%C3%A9"], - { path: "/caf%C3%A9", index: 0, params: { test: "café" } }, + { path: "/caf%C3%A9", index: 0, params: { test: "caf%C3%A9" } }, ], ], [ [{}, null], [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b", { encode: (x) => x }], + [{ test: "a+b" }, "/a+b"], [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], @@ -272,7 +272,7 @@ const TESTS: Test[] = [ [ [{}, null], [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b", { encode: (x) => x }], + [{ test: "a+b" }, "/a+b"], [{ test: "a+b" }, "/test", { encode: () => "test" }], [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], ], @@ -1034,8 +1034,13 @@ const TESTS: Test[] = [ [ [{ test: "" }, "/"], [{ test: "abc" }, "/abc"], - [{ test: "abc/123" }, "/abc%2F123"], - [{ test: "abc/123/456" }, "/abc%2F123%2F456"], + [{ test: "abc/123" }, "/abc/123"], + [{ test: "abc/123" }, "/abc%2F123", { encode: encodeURIComponent }], + [ + { test: "abc/123/456" }, + "/abc%2F123%2F456", + { encode: encodeURIComponent }, + ], ], ], [ @@ -1639,7 +1644,7 @@ const TESTS: Test[] = [ ")", ], [["/route(\\123\\)", ["/route(\\123\\)", "123\\"]]], - [[["123\\"], "/route(\\123\\)", { encode: (x) => x }]], + [[["123\\"], "/route(\\123\\)"]], ], [ "{/login}?", @@ -2281,8 +2286,8 @@ const TESTS: Test[] = [ ], [["/café", ["/café", "café"]]], [ - [{ foo: "café" }, "/café", { encode: (x) => x }], - [{ foo: "café" }, "/caf%C3%A9"], + [{ foo: "café" }, "/café"], + [{ foo: "café" }, "/caf%C3%A9", { encode: encodeURIComponent }], ], ], [ @@ -2792,7 +2797,7 @@ describe("path-to-regexp", () => { it("should throw on missing name", () => { expect(() => { pathToRegexp.pathToRegexp("/:(test)"); - }).toThrow(new TypeError("Missing parameter name at 1")); + }).toThrow(new TypeError("Missing parameter name at 2")); }); it("should throw on nested groups", () => { diff --git a/src/index.ts b/src/index.ts index d7517da..60f8f5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ const DEFAULT_PREFIXES = "./"; const DEFAULT_DELIMITER = "/"; const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; const NOOP_VALUE = (value: string) => value; -const NAME_RE = /^[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$]$/u; +const ID_START = /^[$_\p{ID_Start}]$/u; +const ID_CONTINUE = /^[$_\u200C\u200D\p{ID_Continue}]$/u; /** * Encode a string into another string. @@ -133,56 +134,55 @@ function lexer(str: string) { } if (char === ":") { - let name = ""; - let j = i + 1; + let name = chars[++i]; - while (NAME_RE.test(chars[j])) { - name += chars[j++]; + if (!ID_START.test(chars[i])) { + throw new TypeError(`Missing parameter name at ${i}`); } - if (!name) throw new TypeError(`Missing parameter name at ${i}`); + while (ID_CONTINUE.test(chars[++i])) { + name += chars[i]; + } tokens.push({ type: "NAME", index: i, value: name }); - i = j; continue; } if (char === "(") { + const pos = i++; let count = 1; let pattern = ""; - let j = i + 1; - if (chars[j] === "?") { - throw new TypeError(`Pattern cannot start with "?" at ${j}`); + if (chars[i] === "?") { + throw new TypeError(`Pattern cannot start with "?" at ${i}`); } - while (j < chars.length) { - if (chars[j] === "\\") { - pattern += chars[j++] + chars[j++]; + while (i < chars.length) { + if (chars[i] === "\\") { + pattern += chars[i++] + chars[i++]; continue; } - if (chars[j] === ")") { + if (chars[i] === ")") { count--; if (count === 0) { - j++; + i++; break; } - } else if (chars[j] === "(") { + } else if (chars[i] === "(") { count++; - if (chars[j + 1] !== "?") { - throw new TypeError(`Capturing groups are not allowed at ${j}`); + if (chars[i + 1] !== "?") { + throw new TypeError(`Capturing groups are not allowed at ${i}`); } } - pattern += chars[j++]; + pattern += chars[i++]; } - if (count) throw new TypeError(`Unbalanced pattern at ${i}`); - if (!pattern) throw new TypeError(`Missing pattern at ${i}`); + if (count) throw new TypeError(`Unbalanced pattern at ${pos}`); + if (!pattern) throw new TypeError(`Missing pattern at ${pos}`); tokens.push({ type: "PATTERN", index: i, value: pattern }); - i = j; continue; } @@ -440,7 +440,7 @@ function compileTokens

( options: CompileOptions, ): PathFunction

{ const { - encode = encodeURIComponent, + encode = NOOP_VALUE, validate = true, loose = DEFAULT_DELIMITER, } = options; @@ -514,7 +514,7 @@ function matchRegexp

( keys: Key[], options: MatchOptions, ): MatchFunction

{ - const { decode = decodeURIComponent, loose = DEFAULT_DELIMITER } = options; + const { decode = NOOP_VALUE, loose = DEFAULT_DELIMITER } = options; const stringify = toStringify(loose); const decoders = keys.map((key) => { From c6d683e685b6d9c63de0e6ccbcbcea87a2cff19f Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 23 May 2024 13:02:21 -0700 Subject: [PATCH 17/55] Reuse replacer function --- src/index.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 60f8f5f..2231049 100644 --- a/src/index.ts +++ b/src/index.ts @@ -561,24 +561,20 @@ function escape(str: string) { } /** - * Escape and repeat a string for regular expressions. + * Escape and repeat loose characters for regular expressions. */ -function repeat(str: string) { - return `${escape(str)}+`; +function looseReplacer(value: string, loose: string) { + return loose ? `${escape(value)}+` : escape(value); } /** * Encode all non-delimiter characters using the encode function. */ function toStringify(loose: string) { - if (loose) { - const re = new RegExp(`[^${escape(loose)}]+|(.)`, "g"); - const replacer = (value: string, loose: string) => - loose ? repeat(value) : escape(value); - return (value: string) => value.replace(re, replacer); - } + if (!loose) return escape; - return escape; + const re = new RegExp(`[^${escape(loose)}]+|(.)`, "g"); + return (value: string) => value.replace(re, looseReplacer); } /** From 46d8ff9577b8adbbe58eb3d2dd927c8ccf4d4a05 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Fri, 24 May 2024 17:17:20 -0700 Subject: [PATCH 18/55] Rewrite test suite for simpler editing --- src/index.spec.ts | 5332 +++++++++++++++++++++++---------------------- src/index.ts | 30 +- 2 files changed, 2766 insertions(+), 2596 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 759f5f4..54a574d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,2736 +1,2924 @@ import { describe, it, expect } from "vitest"; -import * as util from "util"; import * as pathToRegexp from "./index"; -type Test = [ - pathToRegexp.Path, - pathToRegexp.MatchOptions | undefined, - pathToRegexp.Token[], - Array<[string, (string | undefined)[] | null, pathToRegexp.Match?]>, - Array<[any, string | null, pathToRegexp.CompileOptions?]>, +interface ParserTestSet { + path: string; + options?: pathToRegexp.ParseOptions; + expected: pathToRegexp.Token[]; +} + +interface CompileTestSet { + path: string; + options?: pathToRegexp.CompileOptions; + tests: Array<{ + input: pathToRegexp.ParamData | undefined; + expected: string | null; + }>; +} + +interface MatchTestSet { + path: pathToRegexp.Path; + options?: pathToRegexp.MatchOptions; + tests: Array<{ + input: string; + matches: (string | undefined)[] | null; + expected: pathToRegexp.Match; + }>; +} + +const PARSER_TESTS: ParserTestSet[] = [ + { + path: "/", + expected: ["/"], + }, +]; + +const COMPILE_TESTS: CompileTestSet[] = [ + { + path: "/", + tests: [ + { input: undefined, expected: "/" }, + { input: {}, expected: "/" }, + { input: { id: "123" }, expected: "/" }, + ], + }, + { + path: "/test", + tests: [ + { input: undefined, expected: "/test" }, + { input: {}, expected: "/test" }, + { input: { id: "123" }, expected: "/test" }, + ], + }, + { + path: "/test/", + tests: [ + { input: undefined, expected: "/test/" }, + { input: {}, expected: "/test/" }, + { input: { id: "123" }, expected: "/test/" }, + ], + }, + { + path: "/:test", + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: null }, // Requires encoding. + ], + }, + { + path: "/:test", + options: { validate: false }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + ], + }, + { + path: "/:test", + options: { encode: encodeURIComponent }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, + ], + }, + { + path: "/:test", + options: { encode: () => "static" }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/static" }, + { input: { test: "123/xyz" }, expected: "/static" }, + ], + }, + { + path: "/:test?", + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: undefined }, expected: "" }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: null }, // Requires encoding. + ], + }, + { + path: "/:test(.*)", + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "" }, expected: "/" }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + ], + }, ]; /** * An array of test cases with expected inputs and outputs. */ -const TESTS: Test[] = [ +const MATCH_TESTS: MatchTestSet[] = [ /** * Simple paths. */ - [ - "/", - undefined, - ["/"], - [ - ["/", ["/"], { path: "/", index: 0, params: {} }], - ["/route", null, false], - ], - [ - [undefined, "/"], - [{}, "/"], - [{ id: 123 }, "/"], - ], - ], - [ - "/test", - undefined, - ["/test"], - [ - ["/test", ["/test"], { path: "/test", index: 0, params: {} }], - ["/route", null, false], - ["/test/route", null, false], - ["/test/", ["/test/"], { path: "/test/", index: 0, params: {} }], - ], - [ - [undefined, "/test"], - [{}, "/test"], - ], - ], - [ - "/test/", - undefined, - ["/test/"], - [ - ["/test", null], - ["/test/", ["/test/"]], - ["/test//", ["/test//"]], - ], - [[undefined, "/test/"]], - ], + { + path: "/", + tests: [ + { + input: "/", + matches: ["/"], + expected: { path: "/", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + ], + }, + { + path: "/test", + tests: [ + { + input: "/test", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + { input: "/test/route", matches: null, expected: false }, + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", index: 0, params: {} }, + }, + ], + }, + { + path: "/test/", + tests: [ + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + { input: "/test", matches: null, expected: false }, + { + input: "/test//", + matches: ["/test//"], + expected: { path: "/test//", index: 0, params: {} }, + }, + ], + }, + { + path: "/:test", + 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: null, + 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%C3%A9" }, + }, + }, + { + input: "/;,:@&=+$-_.!~*()", + matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], + expected: { + path: "/;,:@&=+$-_.!~*()", + index: 0, + params: { test: ";,:@&=+$-_.!~*()" }, + }, + }, + ], + }, /** * Case-sensitive paths. */ - [ - "/test", - { + { + path: "/test", + options: { sensitive: true, }, - ["/test"], - [ - ["/test", ["/test"]], - ["/TEST", null], - ], - [[undefined, "/test"]], - ], - [ - "/TEST", - { + tests: [ + { + input: "/test", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { input: "/TEST", matches: null, expected: false }, + ], + }, + { + path: "/TEST", + options: { sensitive: true, }, - ["/TEST"], - [ - ["/test", null], - ["/TEST", ["/TEST"]], + tests: [ + { + input: "/TEST", + matches: ["/TEST"], + expected: { path: "/TEST", index: 0, params: {} }, + }, + { input: "/test", matches: null, expected: false }, ], - [[undefined, "/TEST"]], - ], + }, /** - * Strict mode. + * Non-trailing mode. */ - [ - "/test", - { + { + 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, }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test/", null], - ["/TEST", ["/TEST"]], - ], - [[undefined, "/test"]], - ], - [ - "/test/", - { + 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, }, - ["/test/"], - [ - ["/test", null], - ["/test/", ["/test/"]], - ["/test//", ["/test//"]], - ["/test/route", null], + 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" } }, + }, ], - [[undefined, "/test/"]], - ], + }, /** * Non-ending mode. */ - [ - "/test", - { + { + path: "/test", + options: { end: false, }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test/", ["/test/"]], - ["/test/route", ["/test"]], - ["/route", null], - ], - [[undefined, "/test"]], - ], - [ - "/test/", - { + 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, }, - ["/test/"], - [ - ["/test", null], - ["/test/route", null], - ["/test//route", ["/test/"]], - ["/test//", ["/test//"]], - ["/foo//bar", null], - ], - [[undefined, "/test/"]], - ], - [ - "/:test", - { + 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, }, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - [ - "/route", - ["/route", "route"], - { path: "/route", index: 0, params: { test: "route" } }, - ], - [ - "/caf%C3%A9", - ["/caf%C3%A9", "caf%C3%A9"], - { path: "/caf%C3%A9", index: 0, params: { test: "caf%C3%A9" } }, - ], - ], - [ - [{}, null], - [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b"], - [{ test: "a+b" }, "/test", { encode: () => "test" }], - [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], - ], - ], - [ - "/:test/", - { + 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%C3%A9" }, + }, + }, + ], + }, + { + path: "/:test/", + options: { end: false, }, - [ + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + 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: ["/route/", "route"], + expected: { path: "/route/", index: 0, params: { test: "route" } }, }, - "/", - ], - [ - ["/route", null], - ["/route/", ["/route/", "route"]], ], - [[{ test: "abc" }, "/abc/"]], - ], - [ - "", - { + }, + { + path: "", + options: { end: false, }, - [], - [ - ["", [""]], - ["/", ["/"]], - ["route", null], - ["/route", [""]], - ["/route/", [""]], + 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: {} }, + }, ], - [[undefined, ""]], - ], + }, /** * Non-starting mode. */ - [ - "/test", - { + { + path: "/test", + options: { start: false, }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test/", ["/test/"]], - ["/route/test", ["/test"]], - ["/test/route", null], - ["/route/test/deep", null], - ["/route", null], - ], - [[undefined, "/test"]], - ], - [ - "/test/", - { + 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, }, - ["/test/"], - [ - ["/test", null], - ["/test/route", null], - ["/test//route", null], - ["/test//", ["/test//"]], - ["/route/test/", ["/test/"]], - ], - [[undefined, "/test/"]], - ], - [ - "/:test", - { + 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, }, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [["/route", ["/route", "route"]]], - [ - [{}, null], - [{ test: "abc" }, "/abc"], - [{ test: "a+b" }, "/a+b"], - [{ test: "a+b" }, "/test", { encode: () => "test" }], - [{ test: "a+b" }, "/a%2Bb", { encode: encodeURIComponent }], - ], - ], - [ - "/:test/", - { + 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: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + 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" } }, }, - "/", - ], - [ - ["/route", null], - ["/route/", ["/route/", "route"]], ], - [[{ test: "abc" }, "/abc/"]], - ], - [ - "", - { + }, + { + path: "", + options: { start: false, }, - [], - [ - ["", [""]], - ["/", ["/"]], - ["route", [""]], - ["/route", [""]], - ["/route/", ["/"]], + 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: {} }, + }, ], - [[undefined, ""]], - ], + }, /** - * Combine modes. + * Non-ending and non-trailing modes. */ - [ - "/test", - { + { + path: "/test", + options: { end: false, trailing: false, }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test/", ["/test"]], - ["/test/route", ["/test"]], - ], - [[undefined, "/test"]], - ], - [ - "/test/", - { + 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, }, - ["/test/"], - [ - ["/test", null], - ["/test/", ["/test/"]], - ["/test//", ["/test//"]], - ["/test/route", null], - ["/test//route", ["/test/"]], - ], - [[undefined, "/test/"]], - ], - [ - "/test.json", - { + 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, }, - ["/test.json"], - [ - ["/test.json", ["/test.json"]], - ["/test.json.hbs", null], - ["/test.json/route", ["/test.json"]], - ], - [[undefined, "/test.json"]], - ], - [ - "/:test", - { + 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, + }, + { + input: "/route/test//", + matches: null, + expected: false, + }, + { + input: "/route//test", + matches: ["/route/", "route"], + expected: { path: "/route/", index: 0, params: { test: "route" } }, + }, + ], + }, + + /** + * Non-starting and non-ending modes. + */ + { + path: "/test", + options: { + start: false, + 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/route", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { + input: "/route/test", + matches: ["/test"], + expected: { path: "/test", index: 6, params: {} }, + }, + ], + }, + { + 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//", index: 0, params: {} }, + }, + { + input: "/test/route", + matches: null, + expected: false, + }, + { + input: "/route/test/deep", + matches: null, + expected: false, + }, + { + input: "/route/test//deep", + matches: ["/test/"], + expected: { path: "/test/", index: 6, params: {} }, + }, + ], + }, + { + path: "/:test", + options: { + start: false, + 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/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: { + start: false, + 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: ["/test/", "test"], + expected: { path: "/test/", index: 6, params: { test: "test" } }, + }, + { + input: "/route/test//", + matches: ["/test//", "test"], + expected: { path: "/test//", index: 6, params: { test: "test" } }, + }, + ], + }, + + /** + * Arrays of simple paths. + */ + { + path: ["/one", "/two"], + tests: [ + { + input: "/one", + matches: ["/one"], + expected: { path: "/one", index: 0, params: {} }, + }, + { + input: "/two", + matches: ["/two"], + expected: { path: "/two", index: 0, params: {} }, + }, + { + input: "/three", + matches: null, + expected: false, + }, + { + input: "/one/two", + matches: null, + expected: false, + }, + ], + }, + + /** + * Optional. + */ + { + path: "/:test?", + 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///", + matches: ["///route///", "route"], + expected: { path: "///route///", index: 0, params: { test: "route" } }, + }, + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "///", + matches: ["///", undefined], + expected: { path: "///", 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: "/", matches: null, expected: false }, + { input: "///", matches: null, expected: false }, + ], + }, + { + path: "/:test?/bar", + tests: [ + { + input: "/bar", + matches: ["/bar", undefined], + expected: { path: "/bar", index: 0, 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" } }, + }, + { + input: "/foo/bar/", + matches: ["/foo/bar/", "foo"], + expected: { path: "/foo/bar/", index: 0, params: { test: "foo" } }, + }, + ], + }, + { + path: "/:test?-bar", + tests: [ + { + input: "-bar", + matches: ["-bar", undefined], + expected: { path: "-bar", index: 0, 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" } }, + }, + ], + }, + + /** + * Zero or more times. + */ + { + path: "/:test*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "//", + matches: ["//", undefined], + expected: { path: "//", index: 0, params: {} }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, 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"] }, + }, + }, + ], + }, + { + path: "/:test*-bar", + tests: [ + { + input: "-bar", + matches: ["-bar", undefined], + expected: { path: "-bar", index: 0, params: {} }, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/foo-bar", + matches: ["/foo-bar", "foo"], + expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + }, + { + input: "/foo/baz-bar", + matches: ["/foo/baz-bar", "foo/baz"], + expected: { + path: "/foo/baz-bar", + index: 0, + params: { test: ["foo", "baz"] }, + }, + }, + ], + }, + + /** + * One or more times. + */ + { + path: "/:test+", + tests: [ + { + input: "/", + matches: null, + expected: false, + }, + { + input: "//", + matches: null, + expected: false, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, 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"] }, + }, + }, + ], + }, + { + path: "/:test+-bar", + tests: [ + { + input: "-bar", + matches: null, + expected: false, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/foo-bar", + matches: ["/foo-bar", "foo"], + expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + }, + { + input: "/foo/baz-bar", + matches: ["/foo/baz-bar", "foo/baz"], + expected: { + path: "/foo/baz-bar", + index: 0, + params: { test: ["foo", "baz"] }, + }, + }, + ], + }, + + /** + * Custom parameters. + */ + { + path: String.raw`/:test(\d+)`, + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { test: "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, + ], + }, + { + path: String.raw`/:test(\d+)-bar`, + tests: [ + { + input: "-bar", + matches: null, + expected: false, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/abc-bar", + matches: null, + expected: false, + }, + { + input: "/123-bar", + matches: ["/123-bar", "123"], + expected: { path: "/123-bar", index: 0, params: { test: "123" } }, + }, + { + input: "/123/456-bar", + matches: null, + expected: false, + }, + ], + }, + { + path: String.raw`/:test(.*)`, + tests: [ + { + input: "/", + matches: ["/", ""], + expected: { path: "/", index: 0, params: { test: "" } }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { test: "route" } }, + }, + { + input: "/route/123", + matches: ["/route/123", "route/123"], + expected: { + path: "/route/123", + index: 0, + params: { test: "route/123" }, + }, + }, + { + input: "/;,:@&=/+$-_.!/~*()", + matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], + expected: { + path: "/;,:@&=/+$-_.!/~*()", + index: 0, + params: { test: ";,:@&=/+$-_.!/~*()" }, + }, + }, + ], + }, + { + path: "/:test([a-z]+)", + tests: [ + { + input: "/abc", + matches: ["/abc", "abc"], + expected: { path: "/abc", index: 0, params: { test: "abc" } }, + }, + { + input: "/123", + matches: null, + expected: false, + }, + { + input: "/abc/123", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test(this|that)", + tests: [ + { + input: "/this", + matches: ["/this", "this"], + expected: { path: "/this", index: 0, params: { test: "this" } }, + }, + { + input: "/that", + matches: ["/that", "that"], + expected: { path: "/that", index: 0, params: { test: "that" } }, + }, + { + input: "/foo", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test(abc|xyz)*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { test: undefined } }, + }, + { + input: "/abc", + matches: ["/abc", "abc"], + expected: { path: "/abc", index: 0, params: { test: ["abc"] } }, + }, + { + input: "/abc/abc", + matches: ["/abc/abc", "abc/abc"], + expected: { + path: "/abc/abc", + index: 0, + params: { test: ["abc", "abc"] }, + }, + }, + { + input: "/xyz/xyz", + matches: ["/xyz/xyz", "xyz/xyz"], + expected: { + path: "/xyz/xyz", + index: 0, + params: { test: ["xyz", "xyz"] }, + }, + }, + { + input: "/abc/xyz", + matches: ["/abc/xyz", "abc/xyz"], + expected: { + path: "/abc/xyz", + index: 0, + params: { test: ["abc", "xyz"] }, + }, + }, + { + input: "/abc/xyz/abc/xyz", + 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, + expected: false, + }, + ], + }, + + /** + * No prefix characters. + */ + { + path: "test", + tests: [ + { + input: "test", + matches: ["test"], + expected: { path: "test", index: 0, params: {} }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: ":test", + tests: [ + { + input: "route", + matches: ["route", "route"], + expected: { path: "route", index: 0, params: { test: "route" } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "route/", + matches: ["route/", "route"], + expected: { path: "route/", index: 0, params: { test: "route" } }, + }, + ], + }, + { + path: ":test?", + tests: [ + { + input: "test", + matches: ["test", "test"], + expected: { path: "test", index: 0, params: { test: "test" } }, + }, + { + input: "", + matches: ["", undefined], + expected: { path: "", index: 0, params: {} }, + }, + ], + }, + { + path: ":test*", + tests: [ + { + input: "test", + matches: ["test", "test"], + expected: { path: "test", index: 0, params: { test: ["test"] } }, + }, + { + input: "test/test", + matches: ["test/test", "test/test"], + expected: { + path: "test/test", + index: 0, + params: { test: ["test", "test"] }, + }, + }, + { + input: "", + matches: ["", undefined], + expected: { path: "", index: 0, params: { test: undefined } }, + }, + ], + }, + { + path: ":test+", + tests: [ + { + input: "test", + matches: ["test", "test"], + expected: { path: "test", index: 0, params: { test: ["test"] } }, + }, + { + input: "test/test", + matches: ["test/test", "test/test"], + expected: { + path: "test/test", + index: 0, + params: { test: ["test", "test"] }, + }, + }, + { + input: "", + matches: null, + expected: false, + }, + ], + }, + { + path: "{:test/}+", + tests: [ + { + input: "route/", + matches: ["route/", "route"], + expected: { path: "route/", index: 0, params: { test: ["route"] } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "", + matches: null, + expected: false, + }, + { + input: "foo/bar/", + matches: ["foo/bar/", "foo/bar"], + expected: { + path: "foo/bar/", + index: 0, + params: { test: ["foo", "bar"] }, + }, + }, + ], + }, + + /** + * Formats. + */ + { + path: "/test.json", + tests: [ + { + input: "/test.json", + matches: ["/test.json"], + expected: { path: "/test.json", index: 0, params: {} }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test.json", + tests: [ + { + input: "/.json", + matches: null, + expected: false, + }, + { + input: "/test.json", + matches: ["/test.json", "test"], + expected: { path: "/test.json", index: 0, params: { test: "test" } }, + }, + { + input: "/route.json", + matches: ["/route.json", "route"], + expected: { path: "/route.json", index: 0, params: { test: "route" } }, + }, + { + input: "/route.json.json", + matches: ["/route.json.json", "route.json"], + expected: { + path: "/route.json.json", + index: 0, + params: { test: "route.json" }, + }, + }, + ], + }, + + /** + * Format params. + */ + { + path: "/test.:format(\\w+)", + tests: [ + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { path: "/test.html", index: 0, params: { format: "html" } }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: "/test.:format(\\w+).:format(\\w+)", + tests: [ + { + input: "/test.html.json", + matches: ["/test.html.json", "html", "json"], + expected: { + path: "/test.html.json", + index: 0, + params: { format: "json" }, + }, + }, + { + input: "/test.html", + matches: null, + expected: false, + }, + ], + }, + { + path: "/test.:format(\\w+)?", + tests: [ + { + input: "/test", + matches: ["/test", undefined], + expected: { path: "/test", index: 0, params: { format: undefined } }, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { path: "/test.html", index: 0, params: { format: "html" } }, + }, + ], + }, + { + path: "/test.:format(\\w+)+", + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { + path: "/test.html", + index: 0, + params: { format: ["html"] }, + }, + }, + { + input: "/test.html.json", + matches: ["/test.html.json", "html.json"], + expected: { + path: "/test.html.json", + index: 0, + params: { format: ["html", "json"] }, + }, + }, + ], + }, + { + path: "/test{.:format}+", + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { + path: "/test.html", + index: 0, + params: { format: ["html"] }, + }, + }, + { + input: "/test.hbs.html", + matches: ["/test.hbs.html", "hbs.html"], + expected: { + path: "/test.hbs.html", + index: 0, + params: { format: ["hbs", "html"] }, + }, + }, + ], + }, + + /** + * Format and path params. + */ + { + path: "/:test.:format", + tests: [ + { + input: "/route.html", + matches: ["/route.html", "route", "html"], + expected: { + path: "/route.html", + index: 0, + params: { test: "route", format: "html" }, + }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "/route.html.json", + matches: ["/route.html.json", "route", "html.json"], + expected: { + path: "/route.html.json", + index: 0, + params: { test: "route", format: "html.json" }, + }, + }, + ], + }, + { + path: "/:test.:format?", + tests: [ + { + input: "/route", + matches: ["/route", "route", undefined], + 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", format: "json" }, + }, + }, + { + input: "/route.json.html", + matches: ["/route.json.html", "route", "json.html"], + expected: { + path: "/route.json.html", + index: 0, + params: { test: "route", format: "json.html" }, + }, + }, + ], + }, + { + path: "/:test.:format\\z", + tests: [ + { + input: "/route.htmlz", + matches: ["/route.htmlz", "route", "html"], + expected: { + path: "/route.htmlz", + index: 0, + params: { test: "route", format: "html" }, + }, + }, + { + input: "/route.html", + matches: null, + expected: false, + }, + ], + }, + + /** + * Unnamed params. + */ + { + path: "/(\\d+)", + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { "0": "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, + ], + }, + { + path: "/(\\d+)?", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { "0": undefined } }, + }, + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { "0": "123" } }, + }, + ], + }, + { + path: "/route\\(\\\\(\\d+\\\\)\\)", + tests: [ + { + input: "/route(\\123\\)", + matches: ["/route(\\123\\)", "123\\"], + expected: { + path: "/route(\\123\\)", + index: 0, + params: { "0": "123\\" }, + }, + }, + { + input: "/route(\\123)", + matches: null, + expected: false, + }, + ], + }, + { + path: "{/route}?", + tests: [ + { + input: "", + matches: [""], + expected: { path: "", index: 0, params: {} }, + }, + { + input: "/", + matches: ["/"], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "/foo", + matches: null, + expected: false, + }, + { + input: "/route", + matches: ["/route"], + expected: { path: "/route", index: 0, params: {} }, + }, + ], + }, + { + path: "{/(.*)}", + tests: [ + { + input: "/", + matches: ["/", ""], + expected: { path: "/", index: 0, params: { "0": "" } }, + }, + { + input: "/login", + matches: ["/login", "login"], + expected: { path: "/login", index: 0, params: { "0": "login" } }, + }, + ], + }, + + /** + * Standalone modifiers. + */ + { + path: "/?", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { "0": "route" } }, + }, + ], + }, + { + path: "/+", + tests: [ + { + input: "/", + matches: null, + expected: false, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/route/", + matches: ["/route/", "route"], + expected: { path: "/route/", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/route/route", + matches: ["/route/route", "route/route"], + expected: { + path: "/route/route", + index: 0, + params: { "0": ["route", "route"] }, + }, + }, + ], + }, + { + path: "/*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { "0": undefined } }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/route/nested", + matches: ["/route/nested", "route/nested"], + expected: { + path: "/route/nested", + index: 0, + params: { "0": ["route", "nested"] }, + }, + }, + ], + }, + + /** + * Regexps. + */ + { + path: /.*/, + tests: [ + { + input: "/match/anything", + matches: ["/match/anything"], + expected: { path: "/match/anything", index: 0, params: {} }, + }, + ], + }, + { + path: /(.*)/, + tests: [ + { + input: "/match/anything", + matches: ["/match/anything", "/match/anything"], + expected: { + path: "/match/anything", + index: 0, + params: { "0": "/match/anything" }, + }, + }, + ], + }, + { + path: /\/(\d+)/, + tests: [ + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { "0": "123" } }, + }, + ], + }, + + /** + * Mixed inputs. + */ + { + path: ["/one", /\/two/], + tests: [ + { + input: "/one", + matches: ["/one"], + expected: { path: "/one", index: 0, params: {} }, + }, + { + input: "/two", + matches: ["/two"], + expected: { path: "/two", index: 0, params: {} }, + }, + { + input: "/three", + matches: null, + expected: false, + }, + ], + }, + { + path: ["/:test(\\d+)", /(.*)/], + tests: [ + { + input: "/123", + matches: ["/123", "123", undefined], + expected: { path: "/123", index: 0, params: { test: "123" } }, + }, + { + input: "/abc", + matches: ["/abc", undefined, "/abc"], + expected: { path: "/abc", index: 0, params: { "0": "/abc" } }, + }, + ], + }, + + /** + * Correct names and indexes. + */ + { + path: ["/:test", "/route/:test2"], + tests: [ + { + input: "/test", + matches: ["/test", "test", undefined], + expected: { path: "/test", index: 0, params: { test: "test" } }, + }, + { + input: "/route/test", + matches: ["/route/test", undefined, "test"], + expected: { path: "/route/test", index: 0, params: { test2: "test" } }, + }, + ], + }, + { + path: [/^\/([^/]+)$/, /^\/route\/([^/]+)$/], + tests: [ + { + input: "/test", + matches: ["/test", "test", undefined], + expected: { path: "/test", index: 0, params: { 0: "test" } }, + }, + { + input: "/route/test", + matches: ["/route/test", undefined, "test"], + expected: { path: "/route/test", index: 0, params: { 0: "test" } }, + }, + ], + }, + { + path: /(?:.*)/, + tests: [ + { + input: "/anything/you/want", + matches: ["/anything/you/want"], + expected: { path: "/anything/you/want", index: 0, params: {} }, + }, + ], + }, + + /** + * Escaped characters. + */ + { + path: "/\\(testing\\)", + tests: [ + { + input: "/testing", + matches: null, + expected: false, + }, + { + input: "/(testing)", + matches: ["/(testing)"], + expected: { path: "/(testing)", index: 0, params: {} }, + }, + ], + }, + { + path: "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", + tests: [ + { + input: "/.+*?{}=^!:$[]|", + matches: ["/.+*?{}=^!:$[]|"], + expected: { path: "/.+*?{}=^!:$[]|", index: 0, params: {} }, + }, + ], + }, + { + path: "/test\\/:uid(u\\d+)?:cid(c\\d+)?", + tests: [ + { + input: "/test/u123", + matches: ["/test/u123", "u123", undefined], + expected: { path: "/test/u123", index: 0, params: { uid: "u123" } }, + }, + { + input: "/test/c123", + matches: ["/test/c123", undefined, "c123"], + expected: { path: "/test/c123", index: 0, params: { cid: "c123" } }, + }, + ], + }, + + /** + * Unnamed group prefix. + */ + { + path: "/{apple-}?icon-:res(\\d+).png", + tests: [ + { + input: "/icon-240.png", + matches: ["/icon-240.png", "240"], + expected: { path: "/icon-240.png", index: 0, 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" }, + }, + }, + ], + }, + + /** + * Random examples. + */ + { + path: "/:foo/:bar", + tests: [ + { + input: "/match/route", + matches: ["/match/route", "match", "route"], + expected: { + path: "/match/route", + index: 0, + params: { foo: "match", bar: "route" }, + }, + }, + ], + }, + { + path: "/:foo\\(test\\)/bar", + tests: [ + { + input: "/foo(test)/bar", + matches: ["/foo(test)/bar", "foo"], + expected: { path: "/foo(test)/bar", index: 0, params: { foo: "foo" } }, + }, + { + input: "/foo/bar", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:remote([\\w-.]+)/:user([\\w-]+)", + tests: [ + { + input: "/endpoint/user", + matches: ["/endpoint/user", "endpoint", "user"], + expected: { + path: "/endpoint/user", + index: 0, + params: { remote: "endpoint", user: "user" }, + }, + }, + { + input: "/endpoint/user-name", + matches: ["/endpoint/user-name", "endpoint", "user-name"], + expected: { + path: "/endpoint/user-name", + index: 0, + params: { remote: "endpoint", user: "user-name" }, + }, + }, + { + input: "/foo.bar/user-name", + matches: ["/foo.bar/user-name", "foo.bar", "user-name"], + expected: { + path: "/foo.bar/user-name", + index: 0, + params: { remote: "foo.bar", user: "user-name" }, + }, + }, + ], + }, + { + path: "/:foo\\?", + tests: [ + { + input: "/route?", + matches: ["/route?", "route"], + expected: { path: "/route?", index: 0, params: { foo: "route" } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:foo+bar", + tests: [ + { + input: "/foobar", + matches: ["/foobar", "foo"], + expected: { path: "/foobar", index: 0, params: { foo: ["foo"] } }, + }, + { + input: "/foo/bar", + matches: null, + expected: false, + }, + { + input: "/foo/barbar", + matches: ["/foo/barbar", "foo/bar"], + expected: { + path: "/foo/barbar", + index: 0, + params: { foo: ["foo", "bar"] }, + }, + }, + ], + }, + { + path: "\\/:pre?baz", + tests: [ + { + input: "/foobaz", + matches: ["/foobaz", "foo"], + expected: { path: "/foobaz", index: 0, params: { pre: "foo" } }, + }, + { + input: "/baz", + matches: ["/baz", undefined], + expected: { path: "/baz", index: 0, params: { pre: undefined } }, + }, + ], + }, + { + path: "/:foo\\(:bar?\\)", + tests: [ + { + input: "/hello(world)", + matches: ["/hello(world)", "hello", "world"], + expected: { + path: "/hello(world)", + index: 0, + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello()", + matches: ["/hello()", "hello", undefined], + expected: { + path: "/hello()", + index: 0, + params: { foo: "hello", bar: undefined }, + }, + }, + ], + }, + { + path: "/:postType(video|audio|text)(\\+.+)?", + tests: [ + { + input: "/video", + matches: ["/video", "video", undefined], + expected: { path: "/video", index: 0, 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, + expected: false, + }, + ], + }, + { + path: "/:foo?/:bar?-ext", + tests: [ + { + input: "/-ext", + matches: null, + expected: false, + }, + { + input: "-ext", + 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" } }, + }, + { + input: "/foo/bar-ext", + matches: ["/foo/bar-ext", "foo", "bar"], + expected: { + path: "/foo/bar-ext", + index: 0, + params: { foo: "foo", bar: "bar" }, + }, + }, { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/foo/-ext", + matches: null, + expected: false, }, ], - [ - ["/route", ["/route", "route"]], - ["/route/", ["/route", "route"]], - ], - [ - [{}, null], - [{ test: "abc" }, "/abc"], - ], - ], - [ - "/:test/", - { - end: false, - trailing: false, - }, - [ + }, + { + path: "/:required/:optional?-ext", + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/foo-ext", + matches: ["/foo-ext", "foo", undefined], + expected: { path: "/foo-ext", index: 0, params: { required: "foo" } }, }, - "/", - ], - [ - ["/route", null], - ["/route/", ["/route/", "route"]], - ], - [[{ test: "foobar" }, "/foobar/"]], - ], - [ - "/test", - { - start: false, - end: false, - }, - ["/test"], - [ - ["/test", ["/test"]], - ["/test/", ["/test/"]], - ["/test/route", ["/test"]], - ["/route/test/deep", ["/test"]], - ], - [[undefined, "/test"]], - ], - [ - "/test/", - { - start: false, - end: false, - }, - ["/test/"], - [ - ["/test", null], - ["/test/", ["/test/"]], - ["/test//", ["/test//"]], - ["/test/route", null], - ["/route/test/deep", null], - ], - [[undefined, "/test/"]], - ], - [ - "/test.json", - { - start: false, - end: false, - }, - ["/test.json"], - [ - ["/test.json", ["/test.json"]], - ["/test.json.hbs", null], - ["/test.json/route", ["/test.json"]], - ["/route/test.json/deep", ["/test.json"]], - ], - [[undefined, "/test.json"]], - ], - [ - "/:test", - { - start: false, - end: false, - }, - [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/foo/bar-ext", + matches: ["/foo/bar-ext", "foo", "bar"], + expected: { + path: "/foo/bar-ext", + index: 0, + params: { required: "foo", optional: "bar" }, + }, }, - ], - [ - ["/route", ["/route", "route"]], - ["/route/", ["/route/", "route"]], - ], - [ - [{}, null], - [{ test: "abc" }, "/abc"], - ], - ], - [ - "/:test/", - { - end: false, - trailing: false, - }, - [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/foo/-ext", + matches: null, + expected: false, }, - "/", - ], - [ - ["/route", null], - ["/route/", ["/route/", "route"]], ], - [[{ test: "foobar" }, "/foobar/"]], - ], - - /** - * Arrays of simple paths. - */ - [ - ["/one", "/two"], - undefined, - [], - [ - ["/one", ["/one"]], - ["/two", ["/two"]], - ["/three", null], - ["/one/two", null], - ], - [], - ], + }, /** - * Non-ending simple path. + * Unicode matches. */ - [ - "/test", - { - end: false, - }, - ["/test"], - [["/test/route", ["/test"]]], - [[undefined, "/test"]], - ], - - /** - * Single named parameter. - */ - [ - "/:test", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - ["/route", ["/route", "route"]], - ["/another", ["/another", "another"]], - ["/something/else", null], - ["/route.json", ["/route.json", "route.json"]], - ["/something%2Felse", ["/something%2Felse", "something%2Felse"]], - [ - "/something%2Felse%2Fmore", - ["/something%2Felse%2Fmore", "something%2Felse%2Fmore"], - ], - ["/;,:@&=+$-_.!~*()", ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"]], - ], - [ - [{ test: "route" }, "/route"], - [ - { test: "something/else" }, - "/something%2Felse", - { encode: encodeURIComponent }, - ], - [ - { test: "something/else/more" }, - "/something%2Felse%2Fmore", - { encode: encodeURIComponent }, - ], - ], - ], - [ - "/:test", - { - trailing: false, - }, - [ + { + path: "/:foo", + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/café", + matches: ["/café", "café"], + expected: { path: "/café", index: 0, params: { foo: "café" } }, }, ], - [ - ["/route", ["/route", "route"]], - ["/route/", null], - ], - [[{ test: "route" }, "/route"]], - ], - [ - "/:test/", - { - trailing: false, + }, + { + path: "/:foo", + options: { + decode: encodeURIComponent, }, - [ + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/café", + matches: ["/café", "café"], + expected: { path: "/café", index: 0, params: { foo: "caf%C3%A9" } }, }, - "/", ], - [ - ["/route/", ["/route/", "route"]], - ["/route//", ["/route//", "route"]], + }, + { + path: "/café", + tests: [ + { + input: "/café", + matches: ["/café"], + expected: { path: "/café", index: 0, params: {} }, + }, ], - [[{ test: "route" }, "/route/"]], - ], - [ - "/:test", - { - end: false, + }, + { + path: "/café", + options: { + encodePath: encodeURI, }, - [ + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/caf%C3%A9", + matches: ["/caf%C3%A9"], + expected: { path: "/caf%C3%A9", index: 0, params: {} }, }, ], - [ - ["/route.json", ["/route.json", "route.json"]], - ["/route//", ["/route//", "route"]], - ["/foo/bar", ["/foo", "foo"]], - ["/foo//bar", ["/foo/", "foo"]], - ], - [[{ test: "route" }, "/route"]], - ], + }, /** - * Optional named parameter. + * Hostnames. */ - [ - "/:test?", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - ], - [ - [ - "/route", - ["/route", "route"], - { path: "/route", index: 0, params: { test: "route" } }, - ], - ["/route/nested", null, false], - ["/", ["/", undefined], { path: "/", index: 0, params: {} }], - ["//", ["//", undefined], { path: "//", index: 0, params: {} }], - ], - [ - [undefined, ""], - [{ test: "foobar" }, "/foobar"], - ], - ], - [ - "/:test?", - { - trailing: false, + { + path: ":domain.com", + options: { + delimiter: ".", }, - [ + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", + input: "example.com", + matches: ["example.com", "example"], + expected: { + path: "example.com", + index: 0, + params: { domain: "example" }, + }, + }, + { + input: "github.com", + matches: ["github.com", "github"], + expected: { + path: "github.com", + index: 0, + params: { domain: "github" }, + }, }, ], - [ - ["/route", ["/route", "route"]], - ["/", null], // Questionable behaviour. - ["//", null], - ], - [ - [undefined, ""], - [{ test: "foobar" }, "/foobar"], - ], - ], - [ - "/:test?/", - { - trailing: false, + }, + { + path: "mail.:domain.com", + options: { + delimiter: ".", }, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - "/", - ], - [ - ["/route", null], - ["/route/", ["/route/", "route"]], - ["/", ["/", undefined]], - ["//", ["//", undefined]], - ], - [ - [undefined, "/"], - [{ test: "foobar" }, "/foobar/"], - ], - ], - [ - "/:test?/bar", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - "/bar", - ], - [ - ["/bar", ["/bar", undefined]], - ["/foo/bar", ["/foo/bar", "foo"]], - ], - [ - [undefined, "/bar"], - [{ test: "foo" }, "/foo/bar"], - ], - ], - [ - "/:test?-bar", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - "-bar", - ], - [ - ["-bar", ["-bar", undefined]], - ["/-bar", null], - ["/foo-bar", ["/foo-bar", "foo"]], - ], - [ - [undefined, "-bar"], - [{ test: "foo" }, "/foo-bar"], - ], - ], - [ - "/:test*-bar", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "*", - pattern: "[^\\/]+?", - separator: "/", - }, - "-bar", - ], - [ - ["-bar", ["-bar", undefined]], - ["/-bar", null], - ["/foo-bar", ["/foo-bar", "foo"]], - ["/foo/baz-bar", ["/foo/baz-bar", "foo/baz"]], - ], - [ - [{}, "-bar"], - [{ test: [] }, "-bar"], - [{ test: ["foo"] }, "/foo-bar"], - ], - ], - - /** - * Repeated one or more times parameters. - */ - [ - "/:test+", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "+", - pattern: "[^\\/]+?", - separator: "/", - }, - ], - [ - ["/", null, false], - [ - "/route", - ["/route", "route"], - { path: "/route", index: 0, params: { test: ["route"] } }, - ], - [ - "/some/basic/route", - ["/some/basic/route", "some/basic/route"], - { - path: "/some/basic/route", + tests: [ + { + input: "mail.example.com", + matches: ["mail.example.com", "example"], + expected: { + path: "mail.example.com", index: 0, - params: { test: ["some", "basic", "route"] }, + params: { domain: "example" }, }, - ], - ["//", null, false], - ], - [ - [{}, null], - [{ test: ["foobar"] }, "/foobar"], - [{ test: ["a", "b", "c"] }, "/a/b/c"], - ], - ], - [ - "/:test(\\d+)+", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "+", - pattern: "\\d+", - separator: "/", - }, - ], - [ - ["/abc/456/789", null], - ["/123/456/789", ["/123/456/789", "123/456/789"]], - ], - [ - [{ test: ["abc"] }, null], - [{ test: ["123"] }, "/123"], - [{ test: ["1", "2", "3"] }, "/1/2/3"], - ], - ], - [ - "/route.:ext(json|xml)+", - undefined, - [ - "/route", - { - name: "ext", - prefix: ".", - suffix: "", - modifier: "+", - pattern: "json|xml", - separator: ".", - }, - ], - [ - ["/route", null], - ["/route.json", ["/route.json", "json"]], - ["/route.xml.json", ["/route.xml.json", "xml.json"]], - ["/route.html", null], - ], - [ - [{ ext: ["foobar"] }, null], - [{ ext: ["xml"] }, "/route.xml"], - [{ ext: ["xml", "json"] }, "/route.xml.json"], - ], - ], - [ - "/route.:ext(\\w+)/test", - undefined, - [ - "/route", - { - name: "ext", - prefix: ".", - suffix: "", - modifier: "", - pattern: "\\w+", - }, - "/test", - ], - [ - ["/route", null], - ["/route.json", null], - ["/route.xml/test", ["/route.xml/test", "xml"]], - ["/route.json.gz/test", null], - ], - [[{ ext: "xml" }, "/route.xml/test"]], - ], - - /** - * Repeated zero or more times parameters. - */ - [ - "/:test*", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "*", - pattern: "[^\\/]+?", - separator: "/", - }, - ], - [ - ["/", ["/", undefined], { path: "/", index: 0, params: {} }], - ["//", ["//", undefined], { path: "//", index: 0, params: {} }], - [ - "/route", - ["/route", "route"], - { path: "/route", index: 0, params: { test: ["route"] } }, - ], - [ - "/some/basic/route", - ["/some/basic/route", "some/basic/route"], - { - path: "/some/basic/route", + }, + { + input: "mail.github.com", + matches: ["mail.github.com", "github"], + expected: { + path: "mail.github.com", index: 0, - params: { test: ["some", "basic", "route"] }, + params: { domain: "github" }, }, - ], - ], - [ - [{}, ""], - [{ test: [] }, ""], - [{ test: ["foobar"] }, "/foobar"], - [{ test: ["foo", "bar"] }, "/foo/bar"], - ], - ], - [ - "/route.:ext([a-z]+)*", - undefined, - [ - "/route", - { - name: "ext", - prefix: ".", - suffix: "", - modifier: "*", - pattern: "[a-z]+", - separator: ".", }, ], - [ - ["/route", ["/route", undefined]], - ["/route.json", ["/route.json", "json"]], - ["/route.json.xml", ["/route.json.xml", "json.xml"]], - ["/route.123", null], - ], - [ - [{}, "/route"], - [{ ext: [] }, "/route"], - [{ ext: ["123"] }, null], - [{ ext: ["foobar"] }, "/route.foobar"], - [{ ext: ["foo", "bar"] }, "/route.foo.bar"], - ], - ], - - /** - * Custom named parameters. - */ - [ - "/:test(\\d+)", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+", - }, - ], - [ - ["/123", ["/123", "123"]], - ["/abc", null], - ["/123/abc", null], - ], - [ - [{ test: "abc" }, null], - [{ test: "abc" }, "/abc", { validate: false }], - [{ test: "123" }, "/123"], - ], - ], - [ - "/:test(\\d+)", - { - end: false, - }, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+", - }, - ], - [ - ["/123", ["/123", "123"]], - ["/abc", null], - ["/123/abc", ["/123", "123"]], - ], - [[{ test: "123" }, "/123"]], - ], - [ - "/:test(.*)", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: ".*", - }, - ], - [ - ["/anything/goes/here", ["/anything/goes/here", "anything/goes/here"]], - ["/;,:@&=/+$-_.!/~*()", ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"]], - ], - [ - [{ test: "" }, "/"], - [{ test: "abc" }, "/abc"], - [{ test: "abc/123" }, "/abc/123"], - [{ test: "abc/123" }, "/abc%2F123", { encode: encodeURIComponent }], - [ - { test: "abc/123/456" }, - "/abc%2F123%2F456", - { encode: encodeURIComponent }, - ], - ], - ], - [ - "/:route([a-z]+)", - undefined, - [ - { - name: "route", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[a-z]+", - }, - ], - [ - ["/abcde", ["/abcde", "abcde"]], - ["/12345", null], - ], - [ - [{ route: "" }, null], - [{ route: "" }, "/", { validate: false }], - [{ route: "123" }, null], - [{ route: "123" }, "/123", { validate: false }], - [{ route: "abc" }, "/abc"], - ], - ], - [ - "/:route(this|that)", - undefined, - [ - { - name: "route", - prefix: "/", - suffix: "", - modifier: "", - pattern: "this|that", - }, - ], - [ - ["/this", ["/this", "this"]], - ["/that", ["/that", "that"]], - ["/foo", null], - ], - [ - [{ route: "this" }, "/this"], - [{ route: "foo" }, null], - [{ route: "foo" }, "/foo", { validate: false }], - [{ route: "that" }, "/that"], - ], - ], - [ - "/:path(abc|xyz)*", - undefined, - [ - { - name: "path", - prefix: "/", - suffix: "", - modifier: "*", - pattern: "abc|xyz", - separator: "/", - }, - ], - [ - ["/abc", ["/abc", "abc"]], - ["/abc/abc", ["/abc/abc", "abc/abc"]], - ["/xyz/xyz", ["/xyz/xyz", "xyz/xyz"]], - ["/abc/xyz", ["/abc/xyz", "abc/xyz"]], - ["/abc/xyz/abc/xyz", ["/abc/xyz/abc/xyz", "abc/xyz/abc/xyz"]], - ["/xyzxyz", null], - ], - [ - [{ path: ["abc"] }, "/abc"], - [{ path: ["abc", "xyz"] }, "/abc/xyz"], - [{ path: ["xyz", "abc", "xyz"] }, "/xyz/abc/xyz"], - [{ path: ["abc123"] }, null], - [{ path: ["abc123"] }, "/abc123", { validate: false }], - [{ path: ["abcxyz"] }, null], - [{ path: ["abcxyz"] }, "/abcxyz", { validate: false }], - ], - ], - - /** - * Prefixed slashes could be omitted. - */ - [ - "test", - undefined, - ["test"], - [ - ["test", ["test"]], - ["/test", null], - ], - [[undefined, "test"]], - ], - [ - ":test", - undefined, - [ - { - name: "test", - prefix: "", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - ["route", ["route", "route"]], - ["/route", null], - ["route/", ["route/", "route"]], - ], - [ - [{ test: "" }, null], - [{}, null], - [{ test: null }, null], - [{ test: "route" }, "route"], - ], - ], - [ - ":test", - { - trailing: false, + }, + { + path: "mail.:domain?.com", + options: { + delimiter: ".", }, - [ + tests: [ { - name: "test", - prefix: "", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "mail.com", + matches: ["mail.com", undefined], + expected: { path: "mail.com", index: 0, params: { domain: undefined } }, }, - ], - [ - ["route", ["route", "route"]], - ["/route", null], - ["route/", null], - ], - [[{ test: "route" }, "route"]], - ], - [ - ":test", - { - end: false, - }, - [ - { - name: "test", - prefix: "", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - ["route", ["route", "route"]], - ["/route", null], - ["route/", ["route/", "route"]], - ["route/foobar", ["route", "route"]], - ], - [[{ test: "route" }, "route"]], - ], - [ - ":test?", - undefined, - [ - { - name: "test", - prefix: "", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - ], - [ - ["route", ["route", "route"]], - ["/route", null], - ["", ["", undefined]], - ["route/foobar", null], - ], - [ - [{}, ""], - [{ test: "" }, ""], - [{ test: "route" }, "route"], - ], - ], - [ - "{:test/}+", - undefined, - [ - { - name: "test", - prefix: "", - suffix: "/", - modifier: "+", - pattern: "[^\\/]+?", - separator: "/", - }, - ], - [ - ["route/", ["route/", "route"]], - ["/route", null], - ["", null], - ["foo/bar/", ["foo/bar/", "foo/bar"]], - ], - [ - [{}, null], - [{ test: "" }, null], - [{ test: ["route"] }, "route/"], - [{ test: ["foo", "bar"] }, "foo/bar/"], - ], - ], - - /** - * Formats. - */ - [ - "/test.json", - undefined, - ["/test.json"], - [ - ["/test.json", ["/test.json"]], - ["/route.json", null], - ], - [[{}, "/test.json"]], - ], - [ - "/:test.json", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ".json", - ], - [ - ["/.json", null], - ["/test.json", ["/test.json", "test"]], - ["/route.json", ["/route.json", "route"]], - ["/route.json.json", ["/route.json.json", "route.json"]], - ], - [ - [{ test: "" }, null], - [{ test: "foo" }, "/foo.json"], - ], - ], - - /** - * Format params. - */ - [ - "/test.:format(\\w+)", - undefined, - [ - "/test", - { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "\\w+", - }, - ], - [ - ["/test.html", ["/test.html", "html"]], - ["/test.hbs.html", null], - ], - [ - [{}, null], - [{ format: "" }, null], - [{ format: "foo" }, "/test.foo"], - ], - ], - [ - "/test.:format(\\w+).:format(\\w+)", - undefined, - [ - "/test", - { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "\\w+", - }, - { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "\\w+", - }, - ], - [ - ["/test.html", null], - ["/test.hbs.html", ["/test.hbs.html", "hbs", "html"]], - ], - [ - [{ format: "foo.bar" }, null], - [{ format: "foo" }, "/test.foo.foo"], - ], - ], - [ - "/test{.:format}+", - undefined, - [ - "/test", - { - name: "format", - prefix: ".", - suffix: "", - modifier: "+", - pattern: "[^\\/]+?", - separator: ".", - }, - ], - [ - ["/test.html", ["/test.html", "html"]], - ["/test.hbs.html", ["/test.hbs.html", "hbs.html"]], - ], - [ - [{ format: [] }, null], - [{ format: ["foo"] }, "/test.foo"], - [{ format: ["foo", "bar"] }, "/test.foo.bar"], - ], - ], - [ - "/test.:format(\\w+)", - { - end: false, - }, - [ - "/test", { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "\\w+", + input: "mail.example.com", + matches: ["mail.example.com", "example"], + expected: { + path: "mail.example.com", + index: 0, + params: { domain: "example" }, + }, }, - ], - [ - ["/test.html", ["/test.html", "html"]], - ["/test.hbs.html", null], - ], - [[{ format: "foo" }, "/test.foo"]], - ], - [ - "/test.:format.", - undefined, - [ - "/test", { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "mail.github.com", + matches: ["mail.github.com", "github"], + expected: { + path: "mail.github.com", + index: 0, + params: { domain: "github" }, + }, }, - ".", ], - [ - ["/test.html.", ["/test.html.", "html"]], - ["/test.hbs.html", null], - ], - [ - [{ format: "" }, null], - [{ format: "foo" }, "/test.foo."], - ], - ], - - /** - * Format and path params. - */ - [ - "/:test.:format", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - ["/route.html", ["/route.html", "route", "html"]], - ["/route", null], - ["/route.html.json", ["/route.html.json", "route", "html.json"]], - ], - [ - [{}, null], - [{ test: "route", format: "foo" }, "/route.foo"], - ], - ], - [ - "/:test{.:format}?", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - { - name: "format", - prefix: ".", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - ], - [ - ["/route", ["/route", "route", undefined]], - ["/route.json", ["/route.json", "route", "json"]], - ["/route.json.html", ["/route.json.html", "route", "json.html"]], - ], - [ - [{ test: "route" }, "/route"], - [{ test: "route", format: "" }, null], - [{ test: "route", format: "foo" }, "/route.foo"], - ], - ], - [ - "/:test.:format?", - { - end: false, + }, + { + path: "example.:ext", + options: { + delimiter: ".", }, - [ + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "example.com", + matches: ["example.com", "com"], + expected: { path: "example.com", index: 0, params: { ext: "com" } }, }, { - name: "format", - prefix: ".", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", + input: "example.org", + matches: ["example.org", "org"], + expected: { path: "example.org", index: 0, params: { ext: "org" } }, }, ], - [ - ["/route", ["/route", "route", undefined]], - ["/route.json", ["/route.json", "route", "json"]], - ["/route.json.html", ["/route.json.html", "route", "json.html"]], - ], - [ - [{ test: "route" }, "/route"], - [{ test: "route", format: undefined }, "/route"], - [{ test: "route", format: "" }, null], - [{ test: "route", format: "foo" }, "/route.foo"], - ], - ], - [ - "/test.:format(.*)z", - { + }, + { + path: "this is", + options: { + delimiter: " ", end: false, }, - [ - "/test", + tests: [ { - name: "format", - prefix: ".", - suffix: "", - modifier: "", - pattern: ".*", + input: "this is a test", + matches: ["this is"], + expected: { path: "this is", index: 0, params: {} }, + }, + { + input: "this isn't", + matches: null, + expected: false, }, - "z", - ], - [ - ["/test.abc", null], - ["/test.z", ["/test.z", ""]], - ["/test.abcz", ["/test.abcz", "abc"]], - ], - [ - [{}, null], - [{ format: "" }, "/test.z"], - [{ format: "foo" }, "/test.fooz"], ], - ], - - /** - * Unnamed params. - */ - [ - "/(\\d+)", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+", - }, - ], - [ - ["/123", ["/123", "123"]], - ["/abc", null], - ["/123/abc", null], - ], - [ - [{}, null], - [{ "0": "123" }, "/123"], - ], - ], - [ - "/(\\d+)", - { - end: false, - }, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+", - }, - ], - [ - ["/123", ["/123", "123"]], - ["/abc", null], - ["/123/abc", ["/123", "123"]], - ["/123/", ["/123/", "123"]], - ], - [[{ "0": "123" }, "/123"]], - ], - [ - "/(\\d+)?", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "?", - pattern: "\\d+", - }, - ], - [ - ["/", ["/", undefined]], - ["/123", ["/123", "123"]], - ], - [ - [{}, ""], - [{ "0": "123" }, "/123"], - ], - ], - [ - "/(.*)", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "", - pattern: ".*", - }, - ], - [ - ["/", ["/", ""]], - ["/route", ["/route", "route"]], - ["/route/nested", ["/route/nested", "route/nested"]], - ], - [ - [{ "0": "" }, "/"], - [{ "0": "123" }, "/123"], - ], - ], - [ - "/route\\(\\\\(\\d+\\\\)\\)", - undefined, - [ - "/route(\\", - { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "\\d+\\\\", - }, - ")", - ], - [["/route(\\123\\)", ["/route(\\123\\)", "123\\"]]], - [[["123\\"], "/route(\\123\\)"]], - ], - [ - "{/login}?", - undefined, - [ - { - name: "", - prefix: "/login", - suffix: "", - modifier: "?", - pattern: "", - }, - ], - [ - ["/", ["/"]], - ["/login", ["/login"]], - ], - [ - [undefined, ""], - [{ "": "" }, "/login"], - ], - ], - [ - "{/login}", - undefined, - [ - { - name: "", - prefix: "/login", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/", null], - ["/login", ["/login"]], - ], - [[{ "": "" }, "/login"]], - ], - [ - "{/(.*)}", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "", - pattern: ".*", - }, - ], - [ - ["/", ["/", ""]], - ["/login", ["/login", "login"]], - ], - [[{ 0: "test" }, "/test"]], - ], - /** - * Standalone modifiers. - */ - [ - "/*", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "*", - pattern: "[^\\/]+?", - separator: "/", - }, - ], - [ - ["/", ["/", undefined]], - ["/route", ["/route", "route"]], - ["/route/nested", ["/route/nested", "route/nested"]], - ], - [ - [{ 0: null }, ""], - [{ 0: ["x"] }, "/x"], - [{ 0: ["a", "b", "c"] }, "/a/b/c"], - ], - ], - [ - "/+", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "+", - pattern: "[^\\/]+?", - separator: "/", - }, - ], - [ - ["/", null], - ["/x", ["/x", "x"]], - ["/route", ["/route", "route"]], - ["/a/b/c", ["/a/b/c", "a/b/c"]], - ], - [ - [{ 0: "" }, null], - [{ 0: ["x"] }, "/x"], - [{ 0: ["route"] }, "/route"], - [{ 0: ["a", "b", "c"] }, "/a/b/c"], - ], - ], - [ - "/?", - undefined, - [ - { - name: 0, - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - ], - [ - ["/", ["/", undefined]], - ["/x", ["/x", "x"]], - ["/route", ["/route", "route"]], - ], - [ - [{ 0: undefined }, ""], - [{ 0: "x" }, "/x"], - ], - ], + }, /** - * Regexps. + * Prefixes. */ - [/.*/, undefined, [], [["/match/anything", ["/match/anything"]]], []], - [ - /(.*)/, - undefined, - [ + { + path: "{$:foo}{$:bar}?", + tests: [ { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "$x", + matches: ["$x", "x", undefined], + expected: { path: "$x", index: 0, params: { foo: "x" } }, }, - ], - [["/match/anything", ["/match/anything", "/match/anything"]]], - [], - ], - [ - /\/(\d+)/, - undefined, - [ { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "$x$y", + matches: ["$x$y", "x", "y"], + expected: { path: "$x$y", index: 0, params: { foo: "x", bar: "y" } }, }, ], - [ - ["/abc", null], - ["/123", ["/123", "123"]], - ], - [], - ], - - /** - * Mixed arrays. - */ - [ - ["/test", /\/(\d+)/], - undefined, - [ + }, + { + path: "{$:foo}+", + tests: [ + { + input: "$x", + matches: ["$x", "x"], + expected: { path: "$x", index: 0, params: { foo: ["x"] } }, + }, { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "$x$y", + matches: ["$x$y", "x$y"], + expected: { path: "$x$y", index: 0, params: { foo: ["x", "y"] } }, }, ], - [["/test", ["/test", undefined]]], - [], - ], - [ - ["/:test(\\d+)", /(.*)/], - undefined, - [ + }, + { + path: "name/:attr1?{-:attr2}?{-:attr3}?", + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+", + input: "name/test", + matches: ["name/test", "test", undefined, undefined], + expected: { + path: "name/test", + index: 0, + params: { attr1: "test" }, + }, }, { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "name/1", + matches: ["name/1", "1", undefined, undefined], + expected: { + path: "name/1", + index: 0, + params: { attr1: "1" }, + }, }, - ], - [ - ["/123", ["/123", "123", undefined]], - ["/abc", ["/abc", undefined, "/abc"]], - ], - [], - ], - - /** - * Correct names and indexes. - */ - [ - ["/:test", "/route/:test"], - undefined, - [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "name/1-2", + matches: ["name/1-2", "1", "2", undefined], + expected: { + path: "name/1-2", + index: 0, + params: { attr1: "1", attr2: "2" }, + }, }, { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "name/1-2-3", + matches: ["name/1-2-3", "1", "2", "3"], + expected: { + path: "name/1-2-3", + index: 0, + params: { attr1: "1", attr2: "2", attr3: "3" }, + }, }, - ], - [ - ["/test", ["/test", "test", undefined]], - ["/route/test", ["/route/test", undefined, "test"]], - ], - [], - ], - [ - [/^\/([^/]+)$/, /^\/route\/([^/]+)$/], - undefined, - [ { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "name/foo-bar/route", + matches: null, + expected: false, }, { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", + input: "name/test/route", + matches: null, + expected: false, }, ], - [ - ["/test", ["/test", "test", undefined]], - ["/route/test", ["/route/test", undefined, "test"]], - ], - [], - ], - - /** - * Ignore non-matching groups in regexps. - */ - [ - /(?:.*)/, - undefined, - [], - [["/anything/you/want", ["/anything/you/want"]]], - [], - ], + }, /** - * Respect escaped characters. - */ - [ - "/\\(testing\\)", - undefined, - ["/(testing)"], - [ - ["/testing", null], - ["/(testing)", ["/(testing)"]], - ], - [[undefined, "/(testing)"]], - ], - [ - "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", - undefined, - ["/.+*?{}=^!:$[]|"], - [["/.+*?{}=^!:$[]|", ["/.+*?{}=^!:$[]|"]]], - [[undefined, "/.+*?{}=^!:$[]|"]], - ], - [ - "/test\\/:uid(u\\d+)?:cid(c\\d+)?", - undefined, - [ - "/test/", - { - name: "uid", - prefix: "", - suffix: "", - modifier: "?", - pattern: "u\\d+", - }, - { - name: "cid", - prefix: "", - suffix: "", - modifier: "?", - pattern: "c\\d+", - }, - ], - [ - ["/test", null], - ["/test/", ["/test/", undefined, undefined]], - ["/test/u123", ["/test/u123", "u123", undefined]], - ["/test/c123", ["/test/c123", undefined, "c123"]], - ], - [ - [{ uid: "u123" }, "/test/u123"], - [{ cid: "c123" }, "/test/c123"], - [{ cid: "u123" }, null], - ], - ], - - /** - * Unnamed group prefix. + * Nested parentheses. */ - [ - "/{apple-}?icon-:res(\\d+).png", - undefined, - [ - "/", + { + path: "/:test(\\d+(?:\\.\\d+)?)", + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { test: "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, { - name: "", - prefix: "apple-", - suffix: "", - modifier: "?", - pattern: "", + input: "/123.123", + matches: ["/123.123", "123.123"], + expected: { path: "/123.123", index: 0, params: { test: "123.123" } }, }, - "icon-", { - name: "res", - prefix: "", - suffix: "", - modifier: "", - pattern: "\\d+", + input: "/123.abc", + matches: null, + expected: false, }, - ".png", ], - [ - ["/icon-240.png", ["/icon-240.png", "240"]], - ["/apple-icon-240.png", ["/apple-icon-240.png", "240"]], + }, + { + path: "/:test((?!login)[^/]+)", + tests: [ + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { test: "route" } }, + }, + { + input: "/login", + matches: null, + expected: false, + }, ], - [[{ res: "240" }, "/icon-240.png"]], - ], + }, /** - * Random examples. + * https://github.com/pillarjs/path-to-regexp/issues/206 */ - [ - "/:foo/:bar", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - { - name: "bar", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [["/match/route", ["/match/route", "match", "route"]]], - [[{ foo: "a", bar: "b" }, "/a/b"]], - ], - [ - "/:foo\\(test\\)/bar", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - "(test)/bar", - ], - [ - ["/foo(test)/bar", ["/foo(test)/bar", "foo"]], - ["/another/bar", null], - ], - [[{ foo: "foo" }, "/foo(test)/bar"]], - ], - [ - "/:remote([\\w-.]+)/:user([\\w-]+)", - undefined, - [ - { - name: "remote", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[\\w-.]+", - }, - { - name: "user", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[\\w-]+", - }, - ], - [ - ["/endpoint/user", ["/endpoint/user", "endpoint", "user"]], - ["/endpoint/user-name", ["/endpoint/user-name", "endpoint", "user-name"]], - ["/foo.bar/user-name", ["/foo.bar/user-name", "foo.bar", "user-name"]], - ], - [ - [{ remote: "foo", user: "bar" }, "/foo/bar"], - [{ remote: "foo.bar", user: "uno" }, "/foo.bar/uno"], - ], - ], - [ - "/:foo\\?", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - "?", - ], - [["/route?", ["/route?", "route"]]], - [[{ foo: "bar" }, "/bar?"]], - ], - [ - "/:foo+baz", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "+", - pattern: "[^\\/]+?", - separator: "/", - }, - "baz", - ], - [ - ["/foobaz", ["/foobaz", "foo"]], - ["/foo/barbaz", ["/foo/barbaz", "foo/bar"]], - ["/baz", null], - ], - [ - [{ foo: [] }, null], - [{ foo: ["foo"] }, "/foobaz"], - [{ foo: ["foo", "bar"] }, "/foo/barbaz"], - ], - ], - [ - "\\/:pre?baz", - undefined, - [ - "/", - { - name: "pre", - prefix: "", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - "baz", - ], - [ - ["/foobaz", ["/foobaz", "foo"]], - ["/baz", ["/baz", undefined]], - ], - [ - [{}, "/baz"], - [{ pre: "foo" }, "/foobaz"], - ], - ], - [ - "/:foo\\(:bar?\\)", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - "(", - { - name: "bar", - prefix: "", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - ")", - ], - [ - ["/hello(world)", ["/hello(world)", "hello", "world"]], - ["/hello()", ["/hello()", "hello", undefined]], - ], - [ - [{ foo: "hello", bar: "world" }, "/hello(world)"], - [{ foo: "hello" }, "/hello()"], - ], - ], - [ - "/:postType(video|audio|text)(\\+.+)?", - undefined, - [ - { - name: "postType", - prefix: "/", - suffix: "", - modifier: "", - pattern: "video|audio|text", - }, - { - name: 0, - prefix: "", - suffix: "", - modifier: "?", - pattern: "\\+.+", - }, - ], - [ - ["/video", ["/video", "video", undefined]], - ["/video+test", ["/video+test", "video", "+test"]], - ["/video+", null], - ], - [ - [{ postType: "video" }, "/video"], - [{ postType: "random" }, null], - ], - ], - [ - "/:foo?/:bar?-ext", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - { - name: "bar", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", - }, - "-ext", - ], - [ - ["/-ext", null], - ["-ext", ["-ext", undefined, undefined]], - ["/foo-ext", ["/foo-ext", "foo", undefined]], - ["/foo/bar-ext", ["/foo/bar-ext", "foo", "bar"]], - ["/foo/-ext", null], - ], - [ - [{}, "-ext"], - [{ foo: "foo" }, "/foo-ext"], - [{ bar: "bar" }, "/bar-ext"], - [{ foo: "foo", bar: "bar" }, "/foo/bar-ext"], - ], - ], - [ - "/:required/:optional?-ext", - undefined, - [ - { - name: "required", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - { - name: "optional", - prefix: "/", - suffix: "", - modifier: "?", - pattern: "[^\\/]+?", + { + path: "/user(s)?/:user", + tests: [ + { + input: "/user/123", + matches: ["/user/123", undefined, "123"], + expected: { path: "/user/123", index: 0, params: { user: "123" } }, }, - "-ext", - ], - [ - ["/foo-ext", ["/foo-ext", "foo", undefined]], - ["/foo/bar-ext", ["/foo/bar-ext", "foo", "bar"]], - ["/foo/-ext", null], - ], - [[{ required: "foo" }, "/foo-ext"]], - ], - - /** - * Unicode characters. - */ - [ - "/:foo", - undefined, - [ - { - name: "foo", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [["/café", ["/café", "café"]]], - [ - [{ foo: "café" }, "/café"], - [{ foo: "café" }, "/caf%C3%A9", { encode: encodeURIComponent }], - ], - ], - [ - "/café", - undefined, - ["/café"], - [["/café", ["/café"]]], - [[undefined, "/café"]], - ], - [ - "/café", - { encodePath: encodeURI }, - ["/caf%C3%A9"], - [["/caf%C3%A9", ["/caf%C3%A9"]]], - [[undefined, "/caf%C3%A9"]], - ], - [ - "packages/", - undefined, - ["packages/"], - [ - ["packages", null], - ["packages/", ["packages/"]], - ], - [[undefined, "packages/"]], - ], - - /** - * Hostnames. - */ - [ - ":domain.com", - { - delimiter: ".", - }, - [ { - name: "domain", - prefix: "", - suffix: "", - modifier: "", - pattern: "[^\\.]+?", + input: "/users/123", + matches: ["/users/123", "s", "123"], + expected: { + path: "/users/123", + index: 0, + params: { 0: "s", user: "123" }, + }, }, - ".com", - ], - [ - ["example.com", ["example.com", "example"]], - ["github.com", ["github.com", "github"]], ], - [ - [{ domain: "example" }, "example.com"], - [{ domain: "github" }, "github.com"], - ], - ], - [ - "mail.:domain.com", - { - delimiter: ".", - }, - [ - "mail", - { - name: "domain", - prefix: ".", - suffix: "", - modifier: "", - pattern: "[^\\.]+?", - }, - ".com", - ], - [ - ["mail.example.com", ["mail.example.com", "example"]], - ["mail.github.com", ["mail.github.com", "github"]], - ], - [ - [{ domain: "example" }, "mail.example.com"], - [{ domain: "github" }, "mail.github.com"], - ], - ], - [ - "example.:ext", - {}, - [ - "example", - { - name: "ext", - prefix: ".", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", - }, - ], - [ - ["example.com", ["example.com", "com"]], - ["example.org", ["example.org", "org"]], - ], - [ - [{ ext: "com" }, "example.com"], - [{ ext: "org" }, "example.org"], - ], - ], - [ - "this is", - { - delimiter: " ", - end: false, - }, - ["this is"], - [ - ["this is a test", ["this is"]], - ["this isn't", null], + }, + { + path: "/user{s}?/:user", + tests: [ + { + input: "/user/123", + matches: ["/user/123", "123"], + expected: { path: "/user/123", index: 0, params: { user: "123" } }, + }, + { + input: "/users/123", + matches: ["/users/123", "123"], + expected: { path: "/users/123", index: 0, params: { user: "123" } }, + }, ], - [[undefined, "this is"]], - ], + }, /** - * Custom prefixes. + * https://github.com/pillarjs/path-to-regexp/issues/260 */ - [ - "{$:foo}{$:bar}?", - {}, - [ + { + path: ":name*", + tests: [ { - name: "foo", - pattern: "[^\\/]+?", - prefix: "$", - suffix: "", - modifier: "", + input: "foobar", + matches: ["foobar", "foobar"], + expected: { path: "foobar", index: 0, params: { name: ["foobar"] } }, }, { - name: "bar", - pattern: "[^\\/]+?", - prefix: "$", - suffix: "", - modifier: "?", + input: "foo/bar", + matches: ["foo/bar", "foo/bar"], + expected: { + path: "foo/bar", + index: 0, + params: { name: ["foo", "bar"] }, + }, }, ], - [ - ["$x", ["$x", "x", undefined]], - ["$x$y", ["$x$y", "x", "y"]], - ], - [ - [{ foo: "foo" }, "$foo"], - [{ foo: "foo", bar: "bar" }, "$foo$bar"], - ], - ], - [ - "name/:attr1?{-:attr2}?{-:attr3}?", - {}, - [ - "name", + }, + { + path: ":name+", + tests: [ { - name: "attr1", - pattern: "[^\\/]+?", - prefix: "/", - suffix: "", - modifier: "?", + input: "", + matches: null, + expected: false, }, { - name: "attr2", - pattern: "[^\\/]+?", - prefix: "-", - suffix: "", - modifier: "?", + input: "foobar", + matches: ["foobar", "foobar"], + expected: { path: "foobar", index: 0, params: { name: ["foobar"] } }, }, { - name: "attr3", - pattern: "[^\\/]+?", - prefix: "-", - suffix: "", - modifier: "?", + input: "foo/bar", + matches: ["foo/bar", "foo/bar"], + expected: { + path: "foo/bar", + index: 0, + params: { name: ["foo", "bar"] }, + }, }, ], - [ - ["name/test", ["name/test", "test", undefined, undefined]], - ["name/1", ["name/1", "1", undefined, undefined]], - ["name/1-2", ["name/1-2", "1", "2", undefined]], - ["name/1-2-3", ["name/1-2-3", "1", "2", "3"]], - ["name/foo-bar/route", null], - ["name/test/route", null], - ], - [ - [{}, "name"], - [{ attr1: "test" }, "name/test"], - [{ attr2: "attr" }, "name-attr"], - ], - ], + }, /** - * Case-sensitive compile tokensToFunction params. + * Named capturing groups. */ - [ - "/:test(abc)", - { - sensitive: true, - }, - [ + { + path: /\/(?.+)/, + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "abc", + input: "/foo", + matches: ["/foo", "foo"], + expected: { path: "/foo", index: 0, params: { groupname: "foo" } }, }, ], - [ - ["/abc", ["/abc", "abc"]], - ["/ABC", null], - ], - [ - [{ test: "abc" }, "/abc"], - [{ test: "ABC" }, null], - ], - ], - [ - "/:test(abc)", - {}, - [ + }, + { + path: /\/(?.*).(?html|json)/, + tests: [ { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "abc", + input: "/route", + matches: null, + expected: false, }, - ], - [ - ["/abc", ["/abc", "abc"]], - ["/ABC", ["/ABC", "ABC"]], - ], - [ - [{ test: "abc" }, "/abc"], - [{ test: "ABC" }, "/ABC"], - ], - ], - - /** - * Nested parentheses. - */ - [ - "/:test(\\d+(?:\\.\\d+)?)", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "\\d+(?:\\.\\d+)?", - }, - ], - [ - ["/123", ["/123", "123"]], - ["/abc", null], - ["/123/abc", null], - ["/123.123", ["/123.123", "123.123"]], - ["/123.abc", null], - ], - [ - [{ test: "abc" }, null], - [{ test: "123" }, "/123"], - [{ test: "123.123" }, "/123.123"], - [{ test: "123.abc" }, null], - ], - ], - [ - "/:test((?!login)[^/]+)", - undefined, - [ - { - name: "test", - prefix: "/", - suffix: "", - modifier: "", - pattern: "(?!login)[^/]+", - }, - ], - [ - ["/route", ["/route", "route"]], - ["/login", null], - ], - [ - [{ test: "route" }, "/route"], - [{ test: "login" }, null], - ], - ], - - /** - * https://github.com/pillarjs/path-to-regexp/issues/206 - */ - [ - "/user(s)?/:user", - undefined, - [ - "/user", { - name: 0, - prefix: "", - suffix: "", - modifier: "?", - pattern: "s", + input: "/route.txt", + matches: null, + expected: false, }, { - name: "user", - prefix: "/", - suffix: "", - modifier: "", - pattern: "[^\\/]+?", + input: "/route.html", + matches: ["/route.html", "route", "html"], + expected: { + path: "/route.html", + index: 0, + params: { test: "route", format: "html" }, + }, }, - ], - [ - ["/user/123", ["/user/123", undefined, "123"]], - ["/users/123", ["/users/123", "s", "123"]], - ], - [[{ user: "123" }, "/user/123"]], - ], - - /** - * https://github.com/pillarjs/path-to-regexp/issues/260 - */ - [ - ":name*", - undefined, - [ { - name: "name", - prefix: "", - suffix: "", - modifier: "*", - pattern: "[^\\/]+?", - separator: "/", + input: "/route.json", + matches: ["/route.json", "route", "json"], + expected: { + path: "/route.json", + index: 0, + params: { test: "route", format: "json" }, + }, }, ], - [ - ["foobar", ["foobar", "foobar"]], - ["foo/bar", ["foo/bar", "foo/bar"]], - ], - [ - [{ name: ["foobar"] }, "foobar"], - [{ name: ["foo", "bar"] }, "foo/bar"], - ], - ], - [ - ":name+", - undefined, - [ + }, + { + path: /\/(.+)\/(?.+)\/(.+)/, + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test/testData", + matches: null, + expected: false, + }, { - name: "name", - prefix: "", - suffix: "", - modifier: "+", - pattern: "[^\\/]+?", - separator: "/", + input: "/test/testData/extraStuff", + matches: [ + "/test/testData/extraStuff", + "test", + "testData", + "extraStuff", + ], + expected: { + path: "/test/testData/extraStuff", + index: 0, + params: { 0: "test", 1: "extraStuff", groupname: "testData" }, + }, }, ], - [["foobar", ["foobar", "foobar"]]], - [[{ name: ["foobar"] }, "foobar"]], - ], - - /** - * Named capturing groups (available from 1812 version 10) - */ - [ - /\/(?.+)/, - undefined, - [ - { - name: "groupname", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/", null], - ["/foo", ["/foo", "foo"]], - ], - [], - ], - [ - /\/(?.*).(?html|json)/, - undefined, - [ - { - name: "test", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: "format", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/route", null], - ["/route.txt", null], - ["/route.html", ["/route.html", "route", "html"]], - ["/route.json", ["/route.json", "route", "json"]], - ], - [], - ], - [ - /\/(.+)\/(?.+)\/(.+)/, - undefined, - [ - { - name: 0, - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: "groupname", - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - { - name: 1, - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }, - ], - [ - ["/test", null], - ["/test/testData", null], - [ - "/test/testData/extraStuff", - ["/test/testData/extraStuff", "test", "testData", "extraStuff"], - ], - ], - [], - ], + }, ]; /** @@ -2770,6 +2958,12 @@ describe("path-to-regexp", () => { expect(exec(re, "/user/123/show")).toEqual(["/user/123", "123"]); }); + it("should accept parse result as input", () => { + const tokens = pathToRegexp.parse("/user/:id"); + const re = pathToRegexp.pathToRegexp(tokens); + expect(exec(re, "/user/123")).toEqual(["/user/123", "123"]); + }); + it("should throw on non-capturing pattern", () => { expect(() => { pathToRegexp.pathToRegexp("/:foo(?:\\d+(\\.\\d+)?)"); @@ -2807,68 +3001,46 @@ describe("path-to-regexp", () => { }); }); - describe("rules", () => { - TESTS.forEach(([path, opts, tokens, matchCases, compileCases]) => { - describe(util.inspect(path), () => { - const keys: pathToRegexp.Key[] = []; - const re = pathToRegexp.pathToRegexp(path, keys, opts); - - // Parsing and compiling is only supported with string input. - if (typeof path === "string") { - it("should parse", () => { - expect(pathToRegexp.parse(path, opts).tokens).toEqual(tokens); - }); - - describe("compile", () => { - compileCases.forEach(([params, result, options]) => { - const toPath = pathToRegexp.compile(path, { - ...opts, - ...options, - }); - - if (result !== null) { - it("should compile using " + util.inspect(params), () => { - expect(toPath(params)).toEqual(result); - }); - } else { - it("should not compile using " + util.inspect(params), () => { - expect(() => { - toPath(params); - }).toThrow(TypeError); - }); - } - }); - }); + describe.each(PARSER_TESTS)( + "parse $path with $options", + ({ path, options, expected }) => { + it("should parse the path", () => { + const data = pathToRegexp.parse(path, options); + expect(data.tokens).toEqual(expected); + }); + }, + ); + + describe.each(COMPILE_TESTS)( + "compile $path with $options", + ({ path, options, tests }) => { + const toPath = pathToRegexp.compile(path, options); + + it.each(tests)("should compile $input", ({ input, expected }) => { + if (expected === null) { + expect(() => { + toPath(input); + }).toThrow(); } else { - it("should parse keys", () => { - expect(keys).toEqual( - tokens.filter((token) => typeof token !== "string"), - ); - }); + expect(toPath(input)).toEqual(expected); } + }); + }, + ); + + describe.each(MATCH_TESTS)( + "match $path with $options", + ({ path, options, tests }) => { + const keys: pathToRegexp.Key[] = []; + const re = pathToRegexp.pathToRegexp(path, keys, options); + const match = pathToRegexp.match(path, options); - describe("match" + (opts ? " using " + util.inspect(opts) : ""), () => { - matchCases.forEach(([pathname, matches, params]) => { - const message = `should ${ - matches ? "" : "not " - }match ${util.inspect(pathname)}`; - - it(message, () => { - expect(exec(re, pathname)).toEqual(matches); - }); - - if (typeof path === "string" && params !== undefined) { - const match = pathToRegexp.match(path, opts); - - it(message + " params", () => { - expect(match(pathname)).toEqual(params); - }); - } - }); - }); + it.each(tests)("should match $input", ({ input, matches, expected }) => { + expect(exec(re, input)).toEqual(matches); + expect(match(input)).toEqual(expected); }); - }); - }); + }, + ); describe("compile errors", () => { it("should throw when a required param is undefined", () => { diff --git a/src/index.ts b/src/index.ts index 2231049..cfcd8f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -250,15 +250,15 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { } = options; const defaultPattern = `[^${escape(delimiter)}]+?`; const tokens: Token[] = []; - const iter = lexer(str); + const it = lexer(str); let key = 0; let path = ""; do { - const char = iter.tryConsume("CHAR"); - const name = iter.tryConsume("NAME"); - const pattern = iter.tryConsume("PATTERN"); - const modifier = iter.tryConsume("MODIFIER"); + const char = it.tryConsume("CHAR"); + const name = it.tryConsume("NAME"); + const pattern = it.tryConsume("PATTERN"); + const modifier = it.tryConsume("MODIFIER"); if (name || pattern || modifier) { let prefix = char || ""; @@ -287,7 +287,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { continue; } - const value = char || iter.tryConsume("ESCAPED_CHAR"); + const value = char || it.tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; @@ -298,19 +298,17 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { path = ""; } - const open = iter.tryConsume("OPEN"); + const open = it.tryConsume("OPEN"); if (open) { - const prefix = iter.text(); - const name = iter.tryConsume("NAME"); - const pattern = iter.tryConsume("PATTERN"); - const suffix = iter.text(); + const prefix = it.text(); + const name = it.tryConsume("NAME"); + const pattern = it.tryConsume("PATTERN"); + const suffix = it.text(); - iter.consume("CLOSE"); + it.consume("CLOSE"); - const modifier = iter.tryConsume("MODIFIER"); + const modifier = it.tryConsume("MODIFIER"); - // TODO: Create non-matching version of keys to switch on/off in `compile`. - // TODO: Make optional trailing `/` a version of this so the info is in the "token". tokens.push( toKey( encodePath, @@ -325,7 +323,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { continue; } - iter.consume("END"); + it.consume("END"); break; } while (true); From cb27d379bc4d604cf9344ddec4fd5a0f18e4147d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 27 May 2024 21:12:17 -0700 Subject: [PATCH 19/55] Add tests from #270 --- src/index.spec.ts | 131 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 54a574d..428d12d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,15 +1,17 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, TestOptions } from "vitest"; import * as pathToRegexp from "./index"; interface ParserTestSet { path: string; options?: pathToRegexp.ParseOptions; expected: pathToRegexp.Token[]; + testOptions?: TestOptions; } interface CompileTestSet { path: string; options?: pathToRegexp.CompileOptions; + testOptions?: TestOptions; tests: Array<{ input: pathToRegexp.ParamData | undefined; expected: string | null; @@ -19,6 +21,7 @@ interface CompileTestSet { interface MatchTestSet { path: pathToRegexp.Path; options?: pathToRegexp.MatchOptions; + testOptions?: TestOptions; tests: Array<{ input: string; matches: (string | undefined)[] | null; @@ -2919,6 +2922,92 @@ const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + + /** + * https://github.com/pillarjs/path-to-regexp/pull/270 + */ + { + path: "/files/:path*.:ext*", + tests: [ + { + input: "/files/hello/world.txt", + 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 }, + }, + }, + ], + }, + { + path: "#/*", + tests: [ + { + input: "#/", + matches: ["#/", undefined], + expected: { path: "#/", index: 0, params: {} }, + }, + ], + }, + { + path: "/foo/:bar*", + tests: [ + { + input: "/foo/test1//test2", + matches: ["/foo/test1//test2", "test1//test2"], + expected: { + path: "/foo/test1//test2", + index: 0, + params: { bar: ["test1", "test2"] }, + }, + }, + ], + }, + { + path: "/entity/:id/*", + tests: [ + { + input: "/entity/foo", + matches: ["/entity/foo", "foo", undefined], + expected: { path: "/entity/foo", index: 0, params: { id: "foo" } }, + }, + { + input: "/entity/foo/", + matches: ["/entity/foo/", "foo", undefined], + expected: { path: "/entity/foo/", index: 0, params: { id: "foo" } }, + }, + ], + }, + { + path: "/test/*", + tests: [ + { + input: "/test", + matches: ["/test", undefined], + expected: { path: "/test", index: 0, params: {} }, + }, + { + input: "/test/", + matches: ["/test/", undefined], + expected: { path: "/test/", index: 0, params: {} }, + }, + { + input: "/test/route", + matches: ["/test/route", "route"], + expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, + }, + ], + }, ]; /** @@ -3003,8 +3092,8 @@ describe("path-to-regexp", () => { describe.each(PARSER_TESTS)( "parse $path with $options", - ({ path, options, expected }) => { - it("should parse the path", () => { + ({ path, options, expected, testOptions }) => { + it("should parse the path", testOptions, () => { const data = pathToRegexp.parse(path, options); expect(data.tokens).toEqual(expected); }); @@ -3013,32 +3102,38 @@ describe("path-to-regexp", () => { describe.each(COMPILE_TESTS)( "compile $path with $options", - ({ path, options, tests }) => { + ({ path, options, tests, testOptions = {} }) => { const toPath = pathToRegexp.compile(path, options); - it.each(tests)("should compile $input", ({ input, expected }) => { - if (expected === null) { - expect(() => { - toPath(input); - }).toThrow(); - } else { - expect(toPath(input)).toEqual(expected); - } - }); + it.each(tests)( + "should compile $input", + testOptions, + ({ input, expected }) => { + if (expected === null) { + expect(() => toPath(input)).toThrow(); + } else { + expect(toPath(input)).toEqual(expected); + } + }, + ); }, ); describe.each(MATCH_TESTS)( "match $path with $options", - ({ path, options, tests }) => { + ({ path, options, tests, testOptions = {} }) => { const keys: pathToRegexp.Key[] = []; const re = pathToRegexp.pathToRegexp(path, keys, options); const match = pathToRegexp.match(path, options); - it.each(tests)("should match $input", ({ input, matches, expected }) => { - expect(exec(re, input)).toEqual(matches); - expect(match(input)).toEqual(expected); - }); + it.each(tests)( + "should match $input", + testOptions, + ({ input, matches, expected }) => { + expect(exec(re, input)).toEqual(matches); + expect(match(input)).toEqual(expected); + }, + ); }, ); From 75e08fcda89cd90f070cd189f3bea0d0e83d5e9c Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 28 May 2024 20:23:04 -0700 Subject: [PATCH 20/55] Disable standalone modifiers --- src/index.spec.ts | 95 ++++++++--------------------------------------- src/index.ts | 95 ++++++++++++++++++++++++----------------------- 2 files changed, 64 insertions(+), 126 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 428d12d..7973d9e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1961,78 +1961,6 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, - /** - * Standalone modifiers. - */ - { - path: "/?", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": "route" } }, - }, - ], - }, - { - path: "/+", - tests: [ - { - input: "/", - matches: null, - expected: false, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": ["route"] } }, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { "0": ["route"] } }, - }, - { - input: "/route/route", - matches: ["/route/route", "route/route"], - expected: { - path: "/route/route", - index: 0, - params: { "0": ["route", "route"] }, - }, - }, - ], - }, - { - path: "/*", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": ["route"] } }, - }, - { - input: "/route/nested", - matches: ["/route/nested", "route/nested"], - expected: { - path: "/route/nested", - index: 0, - params: { "0": ["route", "nested"] }, - }, - }, - ], - }, - /** * Regexps. */ @@ -2951,6 +2879,9 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "#/*", + testOptions: { + skip: true, + }, tests: [ { input: "#/", @@ -2975,6 +2906,9 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "/entity/:id/*", + testOptions: { + skip: true, + }, tests: [ { input: "/entity/foo", @@ -2990,6 +2924,9 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "/test/*", + testOptions: { + skip: true, + }, tests: [ { input: "/test", @@ -3086,7 +3023,7 @@ describe("path-to-regexp", () => { it("should throw on nested groups", () => { expect(() => { pathToRegexp.pathToRegexp("/{a{b:foo}}"); - }).toThrow(new TypeError("Unexpected OPEN at 3, expected CLOSE")); + }).toThrow(new TypeError("Unexpected { at 3, expected }")); }); }); @@ -3103,12 +3040,12 @@ describe("path-to-regexp", () => { describe.each(COMPILE_TESTS)( "compile $path with $options", ({ path, options, tests, testOptions = {} }) => { - const toPath = pathToRegexp.compile(path, options); - it.each(tests)( "should compile $input", testOptions, ({ input, expected }) => { + const toPath = pathToRegexp.compile(path, options); + if (expected === null) { expect(() => toPath(input)).toThrow(); } else { @@ -3122,14 +3059,14 @@ describe("path-to-regexp", () => { describe.each(MATCH_TESTS)( "match $path with $options", ({ path, options, tests, testOptions = {} }) => { - const keys: pathToRegexp.Key[] = []; - const re = pathToRegexp.pathToRegexp(path, keys, options); - const match = pathToRegexp.match(path, options); - it.each(tests)( "should match $input", testOptions, ({ input, matches, expected }) => { + const keys: pathToRegexp.Key[] = []; + const re = pathToRegexp.pathToRegexp(path, keys, options); + const match = pathToRegexp.match(path, options); + expect(exec(re, input)).toEqual(matches); expect(match(input)).toEqual(expected); }, diff --git a/src/index.ts b/src/index.ts index cfcd8f5..757bcce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,24 +79,40 @@ export interface CompileOptions extends ParseOptions { encode?: Encode; } +type TokenType = + | "{" + | "}" + | "*" + | "+" + | "?" + | "NAME" + | "PATTERN" + | "CHAR" + | "ESCAPED" + | "END" + // Reserved for use. + | "!" + | ";"; + /** * Tokenizer results. */ interface LexToken { - type: - | "OPEN" - | "CLOSE" - | "PATTERN" - | "NAME" - | "CHAR" - | "ESCAPED_CHAR" - | "MODIFIER" - | "RESERVED" - | "END"; + type: TokenType; index: number; value: string; } +const SIMPLE_TOKENS: Record = { + "!": "!", + ";": ";", + "*": "*", + "+": "+", + "?": "?", + "{": "{", + "}": "}", +}; + /** * Tokenize input string. */ @@ -107,29 +123,15 @@ function lexer(str: string) { while (i < chars.length) { const char = chars[i]; + const type = SIMPLE_TOKENS[char]; - if (char === "!" || char === ";" || char === "|") { - tokens.push({ type: "RESERVED", index: i, value: chars[i++] }); - continue; - } - - if (char === "*" || char === "+" || char === "?") { - tokens.push({ type: "MODIFIER", index: i, value: chars[i++] }); + if (type) { + tokens.push({ type, index: i++, value: char }); continue; } if (char === "\\") { - tokens.push({ type: "ESCAPED_CHAR", index: i++, value: chars[i++] }); - continue; - } - - if (char === "{") { - tokens.push({ type: "OPEN", index: i, value: chars[i++] }); - continue; - } - - if (char === "}") { - tokens.push({ type: "CLOSE", index: i, value: chars[i++] }); + tokens.push({ type: "ESCAPED", index: i++, value: chars[i++] }); continue; } @@ -220,13 +222,15 @@ class Iter { text(): string { let result = ""; let value: string | undefined; - while ( - (value = this.tryConsume("CHAR") || this.tryConsume("ESCAPED_CHAR")) - ) { + while ((value = this.tryConsume("CHAR") || this.tryConsume("ESCAPED"))) { result += value; } return result; } + + modifier(): string | undefined { + return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); + } } /** @@ -258,9 +262,8 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { const char = it.tryConsume("CHAR"); const name = it.tryConsume("NAME"); const pattern = it.tryConsume("PATTERN"); - const modifier = it.tryConsume("MODIFIER"); - if (name || pattern || modifier) { + if (name || pattern) { let prefix = char || ""; if (!prefixes.includes(prefix)) { @@ -277,17 +280,17 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { toKey( encodePath, delimiter, - name || key++, + name || String(key++), pattern || defaultPattern, prefix, "", - modifier, + it.modifier(), ), ); continue; } - const value = char || it.tryConsume("ESCAPED_CHAR"); + const value = char || it.tryConsume("ESCAPED"); if (value) { path += value; continue; @@ -298,26 +301,24 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { path = ""; } - const open = it.tryConsume("OPEN"); + const open = it.tryConsume("{"); if (open) { const prefix = it.text(); const name = it.tryConsume("NAME"); const pattern = it.tryConsume("PATTERN"); const suffix = it.text(); - it.consume("CLOSE"); - - const modifier = it.tryConsume("MODIFIER"); + it.consume("}"); tokens.push( toKey( encodePath, delimiter, - name || (pattern ? key++ : ""), + name || (pattern ? String(key++) : ""), name && !pattern ? defaultPattern : pattern || "", prefix, suffix, - modifier, + it.modifier(), ), ); continue; @@ -333,7 +334,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { function toKey( encode: Encode, delimiter: string, - name: string | number, + name: string, pattern = "", inputPrefix = "", inputSuffix = "", @@ -586,7 +587,7 @@ function flags(options: { sensitive?: boolean }) { * A key is a capture group in the regex. */ export interface Key { - name: string | number; + name: string; prefix: string; suffix: string; pattern: string; @@ -609,7 +610,7 @@ function regexpToRegexp(path: RegExp, keys: Key[]): RegExp { for (const execResult of path.source.matchAll(GROUPS_RE)) { keys.push({ // Use parenthesized substring match if available, index otherwise. - name: execResult[1] || index++, + name: execResult[1] || String(index++), prefix: "", suffix: "", modifier: "", @@ -664,7 +665,7 @@ function tokensToRegexp( if (typeof token === "string") { pattern += stringify(token); } else { - if (token.pattern) keys.push(token); + if (token.name) keys.push(token); pattern += keyToRegexp(token, stringify); } } @@ -685,7 +686,7 @@ function keyToRegexp(key: Key, stringify: Encode): string { const prefix = stringify(key.prefix); const suffix = stringify(key.suffix); - if (key.pattern) { + if (key.name) { if (key.separator) { const mod = key.modifier === "*" ? "?" : ""; const split = stringify(key.separator); From db3beffd99c176c7b16b5b55d7fa0c1c09dec365 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 28 May 2024 20:51:25 -0700 Subject: [PATCH 21/55] Remove support for regexp and arrays --- src/index.spec.ts | 253 +--------------------------------------------- src/index.ts | 95 ++++------------- 2 files changed, 24 insertions(+), 324 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 7973d9e..d18e7fe 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1043,35 +1043,6 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, - /** - * Arrays of simple paths. - */ - { - path: ["/one", "/two"], - tests: [ - { - input: "/one", - matches: ["/one"], - expected: { path: "/one", index: 0, params: {} }, - }, - { - input: "/two", - matches: ["/two"], - expected: { path: "/two", index: 0, params: {} }, - }, - { - input: "/three", - matches: null, - expected: false, - }, - { - input: "/one/two", - matches: null, - expected: false, - }, - ], - }, - /** * Optional. */ @@ -1961,132 +1932,6 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, - /** - * Regexps. - */ - { - path: /.*/, - tests: [ - { - input: "/match/anything", - matches: ["/match/anything"], - expected: { path: "/match/anything", index: 0, params: {} }, - }, - ], - }, - { - path: /(.*)/, - tests: [ - { - input: "/match/anything", - matches: ["/match/anything", "/match/anything"], - expected: { - path: "/match/anything", - index: 0, - params: { "0": "/match/anything" }, - }, - }, - ], - }, - { - path: /\/(\d+)/, - tests: [ - { - input: "/abc", - matches: null, - expected: false, - }, - { - input: "/123", - matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, - }, - ], - }, - - /** - * Mixed inputs. - */ - { - path: ["/one", /\/two/], - tests: [ - { - input: "/one", - matches: ["/one"], - expected: { path: "/one", index: 0, params: {} }, - }, - { - input: "/two", - matches: ["/two"], - expected: { path: "/two", index: 0, params: {} }, - }, - { - input: "/three", - matches: null, - expected: false, - }, - ], - }, - { - path: ["/:test(\\d+)", /(.*)/], - tests: [ - { - input: "/123", - matches: ["/123", "123", undefined], - expected: { path: "/123", index: 0, params: { test: "123" } }, - }, - { - input: "/abc", - matches: ["/abc", undefined, "/abc"], - expected: { path: "/abc", index: 0, params: { "0": "/abc" } }, - }, - ], - }, - - /** - * Correct names and indexes. - */ - { - path: ["/:test", "/route/:test2"], - tests: [ - { - input: "/test", - matches: ["/test", "test", undefined], - expected: { path: "/test", index: 0, params: { test: "test" } }, - }, - { - input: "/route/test", - matches: ["/route/test", undefined, "test"], - expected: { path: "/route/test", index: 0, params: { test2: "test" } }, - }, - ], - }, - { - path: [/^\/([^/]+)$/, /^\/route\/([^/]+)$/], - tests: [ - { - input: "/test", - matches: ["/test", "test", undefined], - expected: { path: "/test", index: 0, params: { 0: "test" } }, - }, - { - input: "/route/test", - matches: ["/route/test", undefined, "test"], - expected: { path: "/route/test", index: 0, params: { 0: "test" } }, - }, - ], - }, - { - path: /(?:.*)/, - tests: [ - { - input: "/anything/you/want", - matches: ["/anything/you/want"], - expected: { path: "/anything/you/want", index: 0, params: {} }, - }, - ], - }, - /** * Escaped characters. */ @@ -2775,82 +2620,6 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, - /** - * Named capturing groups. - */ - { - path: /\/(?.+)/, - tests: [ - { - input: "/foo", - matches: ["/foo", "foo"], - expected: { path: "/foo", index: 0, params: { groupname: "foo" } }, - }, - ], - }, - { - path: /\/(?.*).(?html|json)/, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route.txt", - matches: null, - expected: false, - }, - { - input: "/route.html", - matches: ["/route.html", "route", "html"], - expected: { - path: "/route.html", - index: 0, - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route.json", - matches: ["/route.json", "route", "json"], - expected: { - path: "/route.json", - index: 0, - params: { test: "route", format: "json" }, - }, - }, - ], - }, - { - path: /\/(.+)\/(?.+)\/(.+)/, - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/testData", - matches: null, - expected: false, - }, - { - input: "/test/testData/extraStuff", - matches: [ - "/test/testData/extraStuff", - "test", - "testData", - "extraStuff", - ], - expected: { - path: "/test/testData/extraStuff", - index: 0, - params: { 0: "test", 1: "extraStuff", groupname: "testData" }, - }, - }, - ], - }, - /** * https://github.com/pillarjs/path-to-regexp/pull/270 */ @@ -2952,23 +2721,8 @@ const MATCH_TESTS: MatchTestSet[] = [ */ describe("path-to-regexp", () => { describe("arguments", () => { - it("should work without different call combinations", () => { - pathToRegexp.pathToRegexp("/test"); - pathToRegexp.pathToRegexp("/test", []); - pathToRegexp.pathToRegexp("/test", undefined, {}); - - pathToRegexp.pathToRegexp(/^\/test/); - pathToRegexp.pathToRegexp(/^\/test/, []); - pathToRegexp.pathToRegexp(/^\/test/, undefined, {}); - - pathToRegexp.pathToRegexp(["/a", "/b"]); - pathToRegexp.pathToRegexp(["/a", "/b"], []); - pathToRegexp.pathToRegexp(["/a", "/b"], undefined, {}); - }); - it("should accept an array of keys as the second argument", () => { - const keys: pathToRegexp.Key[] = []; - const re = pathToRegexp.pathToRegexp("/user/:id", keys, { end: false }); + const re = pathToRegexp.pathToRegexp("/user/:id", { end: false }); const expectedKeys = [ { @@ -2980,7 +2734,7 @@ describe("path-to-regexp", () => { }, ]; - expect(keys).toEqual(expectedKeys); + expect(re.keys).toEqual(expectedKeys); expect(exec(re, "/user/123/show")).toEqual(["/user/123", "123"]); }); @@ -3063,8 +2817,7 @@ describe("path-to-regexp", () => { "should match $input", testOptions, ({ input, matches, expected }) => { - const keys: pathToRegexp.Key[] = []; - const re = pathToRegexp.pathToRegexp(path, keys, options); + const re = pathToRegexp.pathToRegexp(path, options); const match = pathToRegexp.match(path, options); expect(exec(re, input)).toEqual(matches); diff --git a/src/index.ts b/src/index.ts index 757bcce..f4d33d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -122,23 +122,23 @@ function lexer(str: string) { let i = 0; while (i < chars.length) { - const char = chars[i]; - const type = SIMPLE_TOKENS[char]; + const value = chars[i]; + const type = SIMPLE_TOKENS[value]; if (type) { - tokens.push({ type, index: i++, value: char }); + tokens.push({ type, index: i++, value }); continue; } - if (char === "\\") { + if (value === "\\") { tokens.push({ type: "ESCAPED", index: i++, value: chars[i++] }); continue; } - if (char === ":") { + if (value === ":") { let name = chars[++i]; - if (!ID_START.test(chars[i])) { + if (!ID_START.test(name)) { throw new TypeError(`Missing parameter name at ${i}`); } @@ -150,7 +150,7 @@ function lexer(str: string) { continue; } - if (char === "(") { + if (value === "(") { const pos = i++; let count = 1; let pattern = ""; @@ -353,7 +353,7 @@ function toKey( * Compile a string to a template function for the path. */ export function compile

( - value: string | TokenData, + value: Path, options: CompileOptions = {}, ) { const data = value instanceof TokenData ? value : parse(value, options); @@ -500,23 +500,21 @@ export function match

( str: Path, options: MatchOptions = {}, ): MatchFunction

{ - const keys: Key[] = []; - const re = pathToRegexp(str, keys, options); - return matchRegexp

(re, keys, options); + const re = pathToRegexp(str, options); + return matchRegexp

(re, options); } /** * Create a path match function from `path-to-regexp` output. */ function matchRegexp

( - re: RegExp, - keys: Key[], + re: PathRegExp, options: MatchOptions, ): MatchFunction

{ const { decode = NOOP_VALUE, loose = DEFAULT_DELIMITER } = options; const stringify = toStringify(loose); - const decoders = keys.map((key) => { + const decoders = re.keys.map((key) => { if (key.separator) { const re = new RegExp( `(${key.pattern})(?:${stringify(key.separator)}|$)`, @@ -543,7 +541,7 @@ function matchRegexp

( for (let i = 1; i < m.length; i++) { if (m[i] === undefined) continue; - const key = keys[i - 1]; + const key = re.keys[i - 1]; const decoder = decoders[i - 1]; params[key.name] = decoder(m[i]); } @@ -600,50 +598,6 @@ export interface Key { */ export type Token = string | Key; -/** - * Pull out keys from a regexp. - */ -function regexpToRegexp(path: RegExp, keys: Key[]): RegExp { - if (!keys) return path; - - let index = 0; - for (const execResult of path.source.matchAll(GROUPS_RE)) { - keys.push({ - // Use parenthesized substring match if available, index otherwise. - name: execResult[1] || String(index++), - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }); - } - - return path; -} - -/** - * Transform an array into a regexp. - */ -function arrayToRegexp( - paths: PathItem[], - keys: Key[], - options: PathToRegexpOptions, -): RegExp { - const parts = paths.map((path) => pathToRegexp(path, keys, options).source); - return new RegExp(`(?:${parts.join("|")})`, flags(options)); -} - -/** - * Create a path regexp from string input. - */ -function stringToRegexp( - path: string, - keys: Key[], - options: PathToRegexpOptions, -) { - return tokensToRegexp(parse(path, options), keys, options); -} - /** * Expose a function for taking tokens and returning a RegExp. */ @@ -699,15 +653,12 @@ function keyToRegexp(key: Key, stringify: Encode): string { } } -/** - * Simple input types. - */ -export type PathItem = string | RegExp | TokenData; - /** * Repeated and simple input types. */ -export type Path = PathItem | PathItem[]; +export type Path = string | TokenData; + +export type PathRegExp = RegExp & { keys: Key[] }; /** * Normalize the given path string, returning a regular expression. @@ -716,13 +667,9 @@ export type Path = PathItem | PathItem[]; * placeholder key descriptions. For example, using `/user/:id`, `keys` will * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. */ -export function pathToRegexp( - path: Path, - keys: Key[] = [], - options: PathToRegexpOptions = {}, -) { - if (path instanceof TokenData) return tokensToRegexp(path, keys, options); - if (path instanceof RegExp) return regexpToRegexp(path, keys); - if (Array.isArray(path)) return arrayToRegexp(path, keys, options); - return stringToRegexp(path, keys, options); +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 }); } From 01086a0ebd6ccfc995116df3e54bfb7d03381c74 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 3 Jun 2024 19:29:46 -0700 Subject: [PATCH 22/55] Add wildcard functionality --- src/index.spec.ts | 85 +++++++++++++++++++++++++++++++++++-------- src/index.ts | 91 +++++++++++++++++++++++++++-------------------- 2 files changed, 124 insertions(+), 52 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index d18e7fe..f41ed69 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -34,6 +34,12 @@ const PARSER_TESTS: ParserTestSet[] = [ path: "/", expected: ["/"], }, + { + path: "/:test", + expected: [ + { name: "test", prefix: "/", suffix: "", pattern: "", modifier: "" }, + ], + }, ]; const COMPILE_TESTS: CompileTestSet[] = [ @@ -61,6 +67,14 @@ const COMPILE_TESTS: CompileTestSet[] = [ { input: { id: "123" }, expected: "/test/" }, ], }, + { + path: "/:0", + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { 0: "123" }, expected: "/123" }, + ], + }, { path: "/:test", tests: [ @@ -2648,9 +2662,6 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "#/*", - testOptions: { - skip: true, - }, tests: [ { input: "#/", @@ -2675,14 +2686,11 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "/entity/:id/*", - testOptions: { - skip: true, - }, tests: [ { input: "/entity/foo", - matches: ["/entity/foo", "foo", undefined], - expected: { path: "/entity/foo", index: 0, params: { id: "foo" } }, + matches: null, + expected: false, }, { input: "/entity/foo/", @@ -2693,14 +2701,11 @@ const MATCH_TESTS: MatchTestSet[] = [ }, { path: "/test/*", - testOptions: { - skip: true, - }, tests: [ { input: "/test", - matches: ["/test", undefined], - expected: { path: "/test", index: 0, params: {} }, + matches: null, + expected: false, }, { input: "/test/", @@ -2712,6 +2717,58 @@ const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test/route", "route"], expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, }, + { + input: "/test/route/nested", + matches: ["/test/route/nested", "route/nested"], + expected: { + path: "/test/route/nested", + index: 0, + params: { "0": ["route", "nested"] }, + }, + }, + ], + }, + + /** + * Asterisk wildcard. + */ + { + path: "/*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { "0": undefined } }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/route/nested", + matches: ["/route/nested", "route/nested"], + expected: { + path: "/route/nested", + index: 0, + params: { "0": ["route", "nested"] }, + }, + }, + ], + }, + { + path: "*", + tests: [ + { + input: "/", + matches: ["/", "/"], + expected: { path: "/", index: 0, params: { "0": ["", ""] } }, + }, + { + input: "/test", + matches: ["/test", "/test"], + expected: { path: "/test", index: 0, params: { "0": ["", "test"] } }, + }, ], }, ]; @@ -2730,7 +2787,7 @@ describe("path-to-regexp", () => { prefix: "/", suffix: "", modifier: "", - pattern: "[^\\/]+?", + pattern: "", }, ]; diff --git a/src/index.ts b/src/index.ts index f4d33d9..fd0f9df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,6 @@ -const DEFAULT_PREFIXES = "./"; const DEFAULT_DELIMITER = "/"; -const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g; const NOOP_VALUE = (value: string) => value; -const ID_START = /^[$_\p{ID_Start}]$/u; -const ID_CONTINUE = /^[$_\u200C\u200D\p{ID_Continue}]$/u; +const ID_CHAR = /^\p{XID_Continue}$/u; /** * Encode a string into another string. @@ -92,6 +89,7 @@ type TokenType = | "END" // Reserved for use. | "!" + | "@" | ";"; /** @@ -105,6 +103,7 @@ interface LexToken { const SIMPLE_TOKENS: Record = { "!": "!", + "@": "@", ";": ";", "*": "*", "+": "+", @@ -136,14 +135,14 @@ function lexer(str: string) { } if (value === ":") { - let name = chars[++i]; + let name = ""; - if (!ID_START.test(name)) { - throw new TypeError(`Missing parameter name at ${i}`); + while (ID_CHAR.test(chars[++i])) { + name += chars[i]; } - while (ID_CONTINUE.test(chars[++i])) { - name += chars[i]; + if (!name) { + throw new TypeError(`Missing parameter name at ${i}`); } tokens.push({ type: "NAME", index: i, value: name }); @@ -248,11 +247,10 @@ export class TokenData { */ export function parse(str: string, options: ParseOptions = {}): TokenData { const { - prefixes = DEFAULT_PREFIXES, + prefixes = "./", delimiter = DEFAULT_DELIMITER, encodePath = NOOP_VALUE, } = options; - const defaultPattern = `[^${escape(delimiter)}]+?`; const tokens: Token[] = []; const it = lexer(str); let key = 0; @@ -265,6 +263,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (name || pattern) { let prefix = char || ""; + const modifier = it.modifier(); if (!prefixes.includes(prefix)) { path += prefix; @@ -281,10 +280,10 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { encodePath, delimiter, name || String(key++), - pattern || defaultPattern, + pattern, prefix, "", - it.modifier(), + modifier, ), ); continue; @@ -301,6 +300,22 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { path = ""; } + const asterisk = it.tryConsume("*"); + if (asterisk) { + tokens.push( + toKey( + encodePath, + delimiter, + String(key++), + `[^${escape(delimiter)}]*`, + "", + "", + asterisk, + ), + ); + continue; + } + const open = it.tryConsume("{"); if (open) { const prefix = it.text(); @@ -315,7 +330,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { encodePath, delimiter, name || (pattern ? String(key++) : ""), - name && !pattern ? defaultPattern : pattern || "", + pattern, prefix, suffix, it.modifier(), @@ -445,6 +460,7 @@ function compileTokens

( } = options; const reFlags = flags(options); const stringify = toStringify(loose); + const keyToRegexp = toKeyRegexp(stringify, data.delimiter); // Compile all the tokens into regexps. const encoders: Array<(data: ParamData) => string> = data.tokens.map( @@ -452,7 +468,7 @@ function compileTokens

( const fn = tokenToFunction(token, encode); if (!validate || typeof token === "string") return fn; - const pattern = keyToRegexp(token, stringify); + const pattern = keyToRegexp(token); const validRe = new RegExp(`^${pattern}$`, reFlags); return (data) => { @@ -516,16 +532,9 @@ function matchRegexp

( const decoders = re.keys.map((key) => { if (key.separator) { - const re = new RegExp( - `(${key.pattern})(?:${stringify(key.separator)}|$)`, - "g", - ); + const re = new RegExp(stringify(key.separator), "g"); - return (value: string) => { - const result: string[] = []; - for (const m of value.matchAll(re)) result.push(decode(m[1])); - return result; - }; + return (value: string) => value.split(re).map(decode); } return decode; @@ -613,6 +622,7 @@ function tokensToRegexp( loose = DEFAULT_DELIMITER, } = options; const stringify = toStringify(loose); + const keyToRegexp = toKeyRegexp(stringify, data.delimiter); let pattern = start ? "^" : ""; for (const token of data.tokens) { @@ -620,7 +630,7 @@ function tokensToRegexp( pattern += stringify(token); } else { if (token.name) keys.push(token); - pattern += keyToRegexp(token, stringify); + pattern += keyToRegexp(token); } } @@ -636,21 +646,26 @@ function tokensToRegexp( /** * Convert a token into a regexp string (re-used for path validation). */ -function keyToRegexp(key: Key, stringify: Encode): string { - const prefix = stringify(key.prefix); - const suffix = stringify(key.suffix); - - if (key.name) { - if (key.separator) { - const mod = key.modifier === "*" ? "?" : ""; - const split = stringify(key.separator); - return `(?:${prefix}((?:${key.pattern})(?:${split}(?:${key.pattern}))*)${suffix})${mod}`; - } else { - return `(?:${prefix}(${key.pattern})${suffix})${key.modifier}`; +function toKeyRegexp(stringify: Encode, delimiter: string) { + const segmentPattern = `[^${escape(delimiter)}]+?`; + + return (key: Key) => { + const prefix = stringify(key.prefix); + const suffix = stringify(key.suffix); + + if (key.name) { + const pattern = key.pattern || segmentPattern; + if (key.separator) { + const mod = key.modifier === "*" ? "?" : ""; + const split = stringify(key.separator); + return `(?:${prefix}((?:${pattern})(?:${split}(?:${pattern}))*)${suffix})${mod}`; + } else { + return `(?:${prefix}(${pattern})${suffix})${key.modifier}`; + } } - } else { + return `(?:${prefix}${suffix})${key.modifier}`; - } + }; } /** From 578b0727fe32ddf219faf9ec3d9bb22976dc468a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 3 Jun 2024 19:36:15 -0700 Subject: [PATCH 23/55] Test loose can be disabled --- src/index.spec.ts | 20 ++++++++++++++++++++ src/index.ts | 5 +---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index f41ed69..f807e48 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -2771,6 +2771,26 @@ const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + + /** + * No loose. + */ + { + path: "/test", + options: { loose: "" }, + tests: [ + { + input: "/test", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { + input: "//test", + matches: null, + expected: false, + }, + ], + }, ]; /** diff --git a/src/index.ts b/src/index.ts index fd0f9df..6615fb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -634,10 +634,7 @@ function tokensToRegexp( } } - if (trailing) { - pattern += `(?:${stringify(data.delimiter)})${loose ? "?" : ""}`; - } - + if (trailing) pattern += `(?:${stringify(data.delimiter)})?`; pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; return new RegExp(pattern, flags(options)); From e796ace0db1938b29d918938c66649d458d76158 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 3 Jun 2024 20:07:45 -0700 Subject: [PATCH 24/55] Default encode/decode, allow disabling --- src/index.spec.ts | 72 +++++++++++++++++++++++++++++++++++++++++------ src/index.ts | 24 ++++++++-------- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index f807e48..88f9caf 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -81,12 +81,22 @@ const COMPILE_TESTS: CompileTestSet[] = [ { input: undefined, expected: null }, { input: {}, expected: null }, { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, // Requires encoding. + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, ], }, { path: "/:test", options: { validate: false }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, + ], + }, + { + path: "/:test", + options: { validate: false, encode: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -116,16 +126,18 @@ const COMPILE_TESTS: CompileTestSet[] = [ }, { path: "/:test?", + options: { encode: false }, tests: [ { input: undefined, expected: "" }, { input: {}, expected: "" }, { input: { test: undefined }, expected: "" }, { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, // Requires encoding. + { input: { test: "123/xyz" }, expected: null }, ], }, { path: "/:test(.*)", + options: { encode: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -134,6 +146,30 @@ const COMPILE_TESTS: CompileTestSet[] = [ { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, + { + path: "/:test*", + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: [] }, expected: "" }, + { input: { test: [""] }, expected: null }, + { input: { test: ["123"] }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: null }, + { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, + ], + }, + { + path: "/:test*", + options: { encode: false }, + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: "" }, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + { input: { test: ["123", "xyz"] }, expected: null }, + ], + }, ]; /** @@ -235,7 +271,7 @@ const MATCH_TESTS: MatchTestSet[] = [ expected: { path: "/caf%C3%A9", index: 0, - params: { test: "caf%C3%A9" }, + params: { test: "café" }, }, }, { @@ -531,7 +567,7 @@ const MATCH_TESTS: MatchTestSet[] = [ expected: { path: "/caf%C3%A9", index: 0, - params: { test: "caf%C3%A9" }, + params: { test: "café" }, }, }, ], @@ -2257,13 +2293,17 @@ const MATCH_TESTS: MatchTestSet[] = [ { path: "/:foo", options: { - decode: encodeURIComponent, + decode: false, }, tests: [ { - input: "/café", - matches: ["/café", "café"], - expected: { path: "/café", index: 0, params: { foo: "caf%C3%A9" } }, + input: "/caf%C3%A9", + matches: ["/caf%C3%A9", "caf%C3%A9"], + expected: { + path: "/caf%C3%A9", + index: 0, + params: { foo: "caf%C3%A9" }, + }, }, ], }, @@ -2771,6 +2811,22 @@ const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + { + path: "*", + options: { decode: false }, + tests: [ + { + input: "/", + matches: ["/", "/"], + expected: { path: "/", index: 0, params: { "0": "/" } }, + }, + { + input: "/test", + matches: ["/test", "/test"], + expected: { path: "/test", index: 0, params: { "0": "/test" } }, + }, + ], + }, /** * No loose. diff --git a/src/index.ts b/src/index.ts index 6615fb9..82961d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,7 +54,7 @@ export interface MatchOptions extends PathToRegexpOptions { /** * Function for decoding strings for params. */ - decode?: Decode; + decode?: Decode | false; } export interface CompileOptions extends ParseOptions { @@ -73,7 +73,7 @@ export interface CompileOptions extends ParseOptions { /** * Function for encoding input strings for output into the path. (default: `encodeURIComponent`) */ - encode?: Encode; + encode?: Encode | false; } type TokenType = @@ -383,20 +383,21 @@ export type PathFunction

= (data?: P) => string; */ function tokenToFunction( token: Token, - encode: Encode, + encode: Encode | false, ): (data: ParamData) => string { if (typeof token === "string") { return () => token; } const optional = token.modifier === "?" || token.modifier === "*"; + const encodeValue = encode || NOOP_VALUE; - if (token.separator) { + if (encode && token.separator) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}/${index}" to be a string`); } - return encode(value); + return encodeValue(value); }; const compile = (value: unknown) => { @@ -429,7 +430,7 @@ function tokenToFunction( if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}" to be a string`); } - return token.prefix + encode(value) + token.suffix; + return token.prefix + encodeValue(value) + token.suffix; }; if (optional) { @@ -454,9 +455,9 @@ function compileTokens

( options: CompileOptions, ): PathFunction

{ const { - encode = NOOP_VALUE, - validate = true, + encode = encodeURIComponent, loose = DEFAULT_DELIMITER, + validate = true, } = options; const reFlags = flags(options); const stringify = toStringify(loose); @@ -527,17 +528,16 @@ function matchRegexp

( re: PathRegExp, options: MatchOptions, ): MatchFunction

{ - const { decode = NOOP_VALUE, loose = DEFAULT_DELIMITER } = options; + const { decode = decodeURIComponent, loose = DEFAULT_DELIMITER } = options; const stringify = toStringify(loose); const decoders = re.keys.map((key) => { - if (key.separator) { + if (decode && key.separator) { const re = new RegExp(stringify(key.separator), "g"); - return (value: string) => value.split(re).map(decode); } - return decode; + return decode || NOOP_VALUE; }); return function match(pathname: string) { From dfa4451816383356d4089e43e4399bccf77184b8 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 3 Jun 2024 20:35:23 -0700 Subject: [PATCH 25/55] Unicode path name character tests --- src/index.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/index.spec.ts b/src/index.spec.ts index 88f9caf..a0b24c1 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -40,6 +40,24 @@ const PARSER_TESTS: ParserTestSet[] = [ { name: "test", prefix: "/", suffix: "", pattern: "", modifier: "" }, ], }, + { + path: "/:0", + expected: [ + { name: "0", prefix: "/", suffix: "", pattern: "", modifier: "" }, + ], + }, + { + path: "/:_", + expected: [ + { name: "_", prefix: "/", suffix: "", pattern: "", modifier: "" }, + ], + }, + { + path: "/:café", + expected: [ + { name: "café", prefix: "/", suffix: "", pattern: "", modifier: "" }, + ], + }, ]; const COMPILE_TESTS: CompileTestSet[] = [ From 8b7440438f726cce7a891f9325dd79a65978347f Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 15:03:11 -0700 Subject: [PATCH 26/55] Explicit prefix and suffix modifiers (#306) --- Readme.md | 274 ++++++++++++++++++---------------------------- src/index.spec.ts | 219 +++++++++++++++++++++--------------- src/index.ts | 209 +++++++++++++---------------------- 3 files changed, 313 insertions(+), 389 deletions(-) diff --git a/Readme.md b/Readme.md index f20eb28..0061eaf 100644 --- a/Readme.md +++ b/Readme.md @@ -16,59 +16,54 @@ npm install path-to-regexp --save ## Usage -```javascript +```js const { pathToRegexp, match, parse, compile } = require("path-to-regexp"); -// pathToRegexp(path, keys?, options?) -// match(path) -// parse(path) -// compile(path) +// pathToRegexp(path, options?) +// match(path, options?) +// parse(path, options?) +// compile(path, options?) ``` ### Path to regexp -The `pathToRegexp` function will return a regular expression object based on the provided `path` argument. It accepts the following arguments: +The `pathToRegexp` function returns a regular expression with `keys` as a property. It accepts the following arguments: -- **path** A string, array of strings, or a regular expression. -- **keys** _(optional)_ An array to populate with keys found in the path. +- **path** A string. - **options** _(optional)_ - - **sensitive** When `true` the regexp will be case sensitive. (default: `false`) - - **strict** When `true` the regexp won't allow an optional trailing delimiter to match. (default: `false`) - - **end** When `true` the regexp will match to the end of the string. (default: `true`) - - **start** When `true` the regexp will match from the beginning of the string. (default: `true`) - - **delimiter** The default delimiter for segments, e.g. `[^/#?]` for `:named` patterns. (default: `'/#?'`) - - **endsWith** Optional character, or list of characters, to treat as "end" characters. - - **encode** A function to encode strings before inserting into `RegExp`. (default: `x => x`) - - **prefixes** List of characters to automatically consider prefixes when parsing. (default: `./`) - -```javascript -const keys = []; -const regexp = pathToRegexp("/foo/:bar", keys); -// regexp = /^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i -// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }] + - **sensitive** Regexp will be case sensitive. (default: `false`) + - **trailing** Regexp allows an optional trailing delimiter to match. (default: `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 repeated an arbitrary number of times. (default: `true`) + - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) + - **encodePath** A function to encode strings before inserting into `RegExp`. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) + +```js +const regexp = pathToRegexp("/foo/:bar"); +// regexp = /^\/+foo(?:\/+([^\/]+?))(?:\/+)?$/i +// keys = [{ name: 'bar', prefix: '', suffix: '', pattern: '', modifier: '' }] ``` -**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). When using paths that contain query strings, you need to escape the question mark (`?`) to ensure it does not flag the parameter as [optional](#optional). +**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). ### Parameters The path argument is used to define parameters and populate keys. -#### Named Parameters +#### Named parameters -Named parameters are defined by prefixing a colon to the parameter name (`:foo`). +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', prefix: '/', ... }, { name: 'bar', prefix: '/', ... }] +// keys = [{ name: 'foo', ... }, { name: 'bar', ... }] regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ] +//=> [ '/test/route', 'test', 'route', index: 0 ] ``` -**Please note:** Parameter names must use "word characters" (`[A-Za-z0-9_]`). - -##### Custom Matching Parameters +##### Custom matching parameters Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: @@ -94,64 +89,49 @@ regexpWord.exec("/users"); **Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. -##### Custom Prefix and Suffix +#### Unnamed parameters -Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: +It is possible to define a parameter without a name. The name will be numerically indexed: ```js -const regexp = pathToRegexp("/:attr1?{-:attr2}?{-:attr3}?"); - -regexp.exec("/test"); -// => ['/test', 'test', undefined, undefined] +const regexp = pathToRegexp("/:foo/(.*)"); +// keys = [{ name: 'foo', ... }, { name: '0', ... }] -regexp.exec("/test-test"); -// => ['/test', 'test', 'test', undefined] +regexp.exec("/test/route"); +//=> [ '/test/route', 'test', 'route', index: 0 ] ``` -#### Unnamed Parameters +##### Custom prefix and suffix -It is possible to write an unnamed parameter that only consists of a regexp. It works the same the named parameter, except it will be numerically indexed: +Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: ```js -const regexp = pathToRegexp("/:foo/(.*)"); -// keys = [{ name: 'foo', ... }, { name: 0, ... }] +const regexp = pathToRegexp("{/:attr1}?{-:attr2}?{-:attr3}?"); -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ] +regexp.exec("/test"); +// => ['/test', 'test', undefined, undefined] + +regexp.exec("/test-test"); +// => ['/test', 'test', 'test', undefined] ``` #### Modifiers -Modifiers must be placed after the parameter (e.g. `/:foo?`, `/(test)?`, `/:foo(test)?`, or `{-:foo(test)}?`). +Modifiers are used after parameters with custom prefixes and suffixes (`{}`). ##### Optional Parameters can be suffixed with a question mark (`?`) to make the parameter optional. ```js -const regexp = pathToRegexp("/:foo/:bar?"); +const regexp = pathToRegexp("/:foo{/:bar}?"); // keys = [{ name: 'foo', ... }, { name: 'bar', prefix: '/', modifier: '?' }] regexp.exec("/test"); -//=> [ '/test', 'test', undefined, index: 0, input: '/test', groups: undefined ] +//=> [ '/test', 'test', undefined, index: 0 ] regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ] -``` - -**Tip:** The prefix is also optional, escape the prefix `\/` to make it required. - -When dealing with query strings, escape the question mark (`?`) so it doesn't mark the parameter as optional. Handling unordered data is outside the scope of this library. - -```js -const regexp = pathToRegexp("/search/:tableName\\?useIndex=true&term=amazing"); - -regexp.exec("/search/people?useIndex=true&term=amazing"); -//=> [ '/search/people?useIndex=true&term=amazing', 'people', index: 0, input: '/search/people?useIndex=true&term=amazing', groups: undefined ] - -// This library does not handle query strings in different orders -regexp.exec("/search/people?term=amazing&useIndex=true"); -//=> null +//=> [ '/test/route', 'test', 'route', index: 0 ] ``` ##### Zero or more @@ -159,14 +139,14 @@ regexp.exec("/search/people?term=amazing&useIndex=true"); Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. ```js -const regexp = pathToRegexp("/:foo*"); +const regexp = pathToRegexp("{/:foo}*"); // keys = [{ name: 'foo', prefix: '/', modifier: '*' }] -regexp.exec("/"); -//=> [ '/', undefined, index: 0, input: '/', groups: undefined ] +regexp.exec("/foo"); +//=> [ '/foo', "foo", index: 0 ] regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0, input: '/bar/baz', groups: undefined ] +//=> [ '/bar/baz', 'bar/baz', index: 0 ] ``` ##### One or more @@ -174,165 +154,125 @@ 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+"); +const regexp = pathToRegexp("{/:foo}+"); // keys = [{ name: 'foo', prefix: '/', modifier: '+' }] regexp.exec("/"); //=> null regexp.exec("/bar/baz"); -//=> [ '/bar/baz','bar/baz', index: 0, input: '/bar/baz', groups: undefined ] +//=> [ '/bar/baz', 'bar/baz', index: 0 ] ``` -### Match - -The `match` function will return a function for transforming paths into parameters: - -```js -// Make sure you consistently `decode` segments. -const fn = match("/user/:id", { decode: decodeURIComponent }); - -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é' } } -``` +#### Wildcard -The `match` function can be used to custom match named parameters. For example, this can be used to whitelist a small number of valid paths: +A wildcard can also be used. It is roughly equivalent to `(.*)` except when decoding in `match` below it splits on the delimiter. ```js -const urlMatch = match("/users/:id/:tab(home|photos|bio)", { - decode: decodeURIComponent, -}); - -urlMatch("/users/1234/photos"); -//=> { path: '/users/1234/photos', index: 0, params: { id: '1234', tab: 'photos' } } - -urlMatch("/users/1234/bio"); -//=> { path: '/users/1234/bio', index: 0, params: { id: '1234', tab: 'bio' } } - -urlMatch("/users/1234/otherstuff"); -//=> false -``` +const regexp = pathToRegexp("/*"); +// keys = [{ name: '0', pattern: '[^\\/]*', modifier: '*' }] -#### Process Pathname - -You should make sure variations of the same path match the expected `path`. Here's one possible solution using `encode`: - -```js -const fn = match("/café", { encode: encodeURI }); +regexp.exec("/"); +//=> [ '/', '', index: 0 ] -fn("/caf%C3%A9"); //=> { path: '/caf%C3%A9', index: 0, params: {} } +regexp.exec("/bar/baz"); +//=> [ '/bar/baz', 'bar/baz', index: 0 ] ``` -**Note:** [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) encodes paths, so `/café` would be normalized to `/caf%C3%A9` and match in the above example. - -##### Alternative Using Normalize - -Sometimes you won't have already normalized paths to use, so you could normalize it yourself before matching: - -```js -/** - * Normalize a pathname for matching, replaces multiple slashes with a single - * slash and normalizes unicode characters to "NFC". When using this method, - * `decode` should be an identity function so you don't decode strings twice. - */ -function normalizePathname(pathname: string) { - return ( - decodeURI(pathname) - // Replaces repeated slashes in the URL. - .replace(/\/+/g, "/") - // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize - // Note: Missing native IE support, may want to skip this step. - .normalize() - ); -} - -// Two possible ways of writing `/café`: -const re = pathToRegexp("/caf\u00E9"); -const input = encodeURI("/cafe\u0301"); - -re.test(input); //=> false -re.test(normalizePathname(input)); //=> true -``` +### Match -### Parse +The `match` function returns a function for transforming paths into parameters: -The `parse` function will return a list of strings and keys from a path string: +- **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 tokens = parse("/route/:foo/(.*)"); - -console.log(tokens[0]); -//=> "/route" - -console.log(tokens[1]); -//=> { name: 'foo', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' } +// Make sure you consistently `decode` segments. +const fn = match("/user/:id", { decode: decodeURIComponent }); -console.log(tokens[2]); -//=> { name: 0, prefix: '/', suffix: '', pattern: '.*', modifier: '' } +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é' } } ``` -**Note:** This method only works with strings. +**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: + - **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`) + - **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) + ```js -// Make sure you encode your path segments consistently. -const toPath = compile("/user/:id", { encode: encodeURIComponent }); +const toPath = compile("/user/:id"); toPath({ id: 123 }); //=> "/user/123" toPath({ id: "café" }); //=> "/user/caf%C3%A9" toPath({ id: ":/" }); //=> "/user/%3A%2F" -// Without `encode`, you need to make sure inputs are encoded correctly. -// (Note: You can use `validate: false` to create an invalid paths.) -const toPathRaw = compile("/user/:id", { validate: false }); +// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. +const toPathRaw = compile("/user/:id", { encode: false }); toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" -toPathRaw({ id: ":/" }); //=> "/user/:/" +toPathRaw({ id: ":/" }); //=> "/user/:/", throws when `validate: false` is not set. -const toPathRepeated = compile("/:segment+"); +const toPathRepeated = compile("{/:segment}+"); -toPathRepeated({ segment: "foo" }); //=> "/foo" +toPathRepeated({ segment: ["foo"] }); //=> "/foo" toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c" const toPathRegexp = compile("/user/:id(\\d+)"); -toPathRegexp({ id: 123 }); //=> "/user/123" toPathRegexp({ id: "123" }); //=> "/user/123" ``` -**Note:** The generated function will throw on invalid input. +## Developers -### Working with Tokens +- If you are rewriting paths with match and compiler, 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: ''`, `trailing: false`, and `sensitive: true`. -Path-To-RegExp exposes the two functions used internally that accept an array of tokens: +### Parse -- `tokensToRegexp(tokens, keys?, options?)` Transform an array of tokens into a matching regular expression. -- `tokensToFunction(tokens)` Transform an array of tokens into a path generator function. +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. -#### Token Information +### Token Information - `name` The name of the token (`string` for named or `number` for unnamed index) - `prefix` The prefix string for the segment (e.g. `"/"`) - `suffix` The suffix string for the segment (e.g. `""`) - `pattern` The RegExp used to match this token (`string`) - `modifier` The modifier character used for the segment (e.g. `?`) +- `separator` _(optional)_ The string used to separate repeated parameters (modifier is `+` or `*`) +- `optional` _(optional)_ A boolean used to indicate whether the parameter is optional (modifier is `?` or `*`) + +## Errors + +An effort has been made to ensure ambiguous paths from previous releases throw an error. This means you might be seeing an error when things worked before. + +### Unexpected `?`, `*`, or `+` + +In previous major versions, `/` or `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. + +This has been made explicit. Assuming `?` as the modifier, if you have a `/` or `.` before the parameter, you want `{.:ext}?` or `{/:ext}?`. If not, you want `{:ext}?`. -## Compatibility with Express <= 4.x +### Unexpected `!`, `@`, or `;` -Path-To-RegExp breaks compatibility with Express <= `4.x`: +These characters have been reserved for future use. -- RegExp special characters can only be used in a parameter - - Express.js 4.x supported `RegExp` special characters regardless of position - this is considered a bug -- Parameters have suffixes that augment meaning - `*`, `+` and `?`. E.g. `/:user*` -- No wildcard asterisk (`*`) - use parameters instead (`(.*)` or `:splat*`) +### Express <= 4.x -## Live Demo +Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways: -You can see a live demo of this library in use at [express-route-tester](http://forbeslindesay.github.io/express-route-tester/). +- The only part of the string that is a regex is within `()`. + - In Express.js 4.x, everything was passed as-is after a simple replacement, so you could write `/[a-z]+` to match `/test`. +- The `?` optional character must be used after `{}`. +- Some characters have new meaning or have been reserved (`{}?*+@!;`). +- The parameter name now supports all unicode identifier characters, previously it was only `[a-z0-9]`. ## License diff --git a/src/index.spec.ts b/src/index.spec.ts index a0b24c1..7f85628 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -37,25 +37,29 @@ const PARSER_TESTS: ParserTestSet[] = [ { path: "/:test", expected: [ - { name: "test", prefix: "/", suffix: "", pattern: "", modifier: "" }, + "/", + { name: "test", prefix: "", suffix: "", pattern: "", modifier: "" }, ], }, { path: "/:0", expected: [ - { name: "0", prefix: "/", suffix: "", pattern: "", modifier: "" }, + "/", + { name: "0", prefix: "", suffix: "", pattern: "", modifier: "" }, ], }, { path: "/:_", expected: [ - { name: "_", prefix: "/", suffix: "", pattern: "", modifier: "" }, + "/", + { name: "_", prefix: "", suffix: "", pattern: "", modifier: "" }, ], }, { path: "/:café", expected: [ - { name: "café", prefix: "/", suffix: "", pattern: "", modifier: "" }, + "/", + { name: "café", prefix: "", suffix: "", pattern: "", modifier: "" }, ], }, ]; @@ -143,7 +147,7 @@ const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "/:test?", + path: "{/:test}?", options: { encode: false }, tests: [ { input: undefined, expected: "" }, @@ -165,7 +169,7 @@ const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "/:test*", + path: "{/:test}*", tests: [ { input: undefined, expected: "" }, { input: {}, expected: "" }, @@ -177,7 +181,7 @@ const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "/:test*", + path: "{/:test}*", options: { encode: false }, tests: [ { input: undefined, expected: "" }, @@ -1115,7 +1119,7 @@ const MATCH_TESTS: MatchTestSet[] = [ * Optional. */ { - path: "/:test?", + path: "{/:test}?", tests: [ { input: "/route", @@ -1145,7 +1149,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test?", + path: "{/:test}?", options: { trailing: false, }, @@ -1165,7 +1169,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test?/bar", + path: "{/:test}?/bar", tests: [ { input: "/bar", @@ -1190,7 +1194,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test?-bar", + path: "{/:test}?-bar", tests: [ { input: "-bar", @@ -1209,12 +1213,32 @@ const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + { + path: "/{:test}?-bar", + tests: [ + { + input: "/-bar", + matches: ["/-bar", undefined], + expected: { path: "/-bar", index: 0, 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" } }, + }, + ], + }, /** * Zero or more times. */ { - path: "/:test*", + path: "{/:test}*", tests: [ { input: "/", @@ -1252,7 +1276,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test*-bar", + path: "{/:test}*-bar", tests: [ { input: "-bar", @@ -1285,7 +1309,7 @@ const MATCH_TESTS: MatchTestSet[] = [ * One or more times. */ { - path: "/:test+", + path: "{/:test}+", tests: [ { input: "/", @@ -1323,7 +1347,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test+-bar", + path: "{/:test}+-bar", tests: [ { input: "-bar", @@ -1479,7 +1503,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test(abc|xyz)*", + path: "{/:test(abc|xyz)}*", tests: [ { input: "/", @@ -1574,7 +1598,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: ":test?", + path: "{:test}?", tests: [ { input: "test", @@ -1589,7 +1613,10 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: ":test*", + path: "{:test}*", + testOptions: { + skip: true, + }, tests: [ { input: "test", @@ -1613,7 +1640,10 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: ":test+", + path: "{:test}+", + testOptions: { + skip: true, + }, tests: [ { input: "test", @@ -1752,7 +1782,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/test.:format(\\w+)?", + path: "/test{.:format(\\w+)}?", tests: [ { input: "/test", @@ -1767,7 +1797,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/test.:format(\\w+)+", + path: "/test{.:format(\\w+)}+", tests: [ { input: "/test", @@ -1855,7 +1885,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:test.:format?", + path: "/:test{.:format}?", tests: [ { input: "/route", @@ -1926,7 +1956,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/(\\d+)?", + path: "{/(\\d+)}?", tests: [ { input: "/", @@ -2029,7 +2059,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/test\\/:uid(u\\d+)?:cid(c\\d+)?", + path: "/test/{:uid(u\\d+)}?{:cid(c\\d+)}?", tests: [ { input: "/test/u123", @@ -2147,7 +2177,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:foo+bar", + path: "{/:foo}+bar", tests: [ { input: "/foobar", @@ -2171,7 +2201,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "\\/:pre?baz", + path: "/{:pre}?baz", tests: [ { input: "/foobaz", @@ -2186,7 +2216,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:foo\\(:bar?\\)", + path: "/:foo\\({:bar}?\\)", tests: [ { input: "/hello(world)", @@ -2209,7 +2239,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:postType(video|audio|text)(\\+.+)?", + path: "/:postType(video|audio|text){(\\+.+)}?", tests: [ { input: "/video", @@ -2233,7 +2263,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:foo?/:bar?-ext", + path: "{/:foo}?{/:bar}?-ext", tests: [ { input: "/-ext", @@ -2271,7 +2301,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:required/:optional?-ext", + path: "/:required{/:optional}?-ext", tests: [ { input: "/foo-ext", @@ -2405,7 +2435,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "mail.:domain?.com", + path: "mail{.:domain}?.com", options: { delimiter: ".", }, @@ -2507,8 +2537,13 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "name/:attr1?{-:attr2}?{-:attr3}?", + path: "name{/:attr1}?{-:attr2}?{-:attr3}?", tests: [ + { + input: "name", + matches: ["name", undefined, undefined, undefined], + expected: { path: "name", index: 0, params: {} }, + }, { input: "name/test", matches: ["name/test", "test", undefined, undefined], @@ -2557,6 +2592,53 @@ const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + { + path: "name{/:attrs;-}*", + tests: [ + { + input: "name", + matches: ["name", undefined], + expected: { path: "name", index: 0, params: {} }, + }, + { + input: "name/1", + matches: ["name/1", "1"], + expected: { + path: "name/1", + index: 0, + params: { attrs: ["1"] }, + }, + }, + { + input: "name/1-2", + matches: ["name/1-2", "1-2"], + expected: { + path: "name/1-2", + index: 0, + params: { attrs: ["1", "2"] }, + }, + }, + { + input: "name/1-2-3", + 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, + expected: false, + }, + { + input: "name/test/route", + matches: null, + expected: false, + }, + ], + }, /** * Nested parentheses. @@ -2611,7 +2693,7 @@ const MATCH_TESTS: MatchTestSet[] = [ * https://github.com/pillarjs/path-to-regexp/issues/206 */ { - path: "/user(s)?/:user", + path: "/user{(s)}?/:user", tests: [ { input: "/user/123", @@ -2645,58 +2727,11 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, - /** - * https://github.com/pillarjs/path-to-regexp/issues/260 - */ - { - path: ":name*", - tests: [ - { - input: "foobar", - matches: ["foobar", "foobar"], - expected: { path: "foobar", index: 0, params: { name: ["foobar"] } }, - }, - { - input: "foo/bar", - matches: ["foo/bar", "foo/bar"], - expected: { - path: "foo/bar", - index: 0, - params: { name: ["foo", "bar"] }, - }, - }, - ], - }, - { - path: ":name+", - tests: [ - { - input: "", - matches: null, - expected: false, - }, - { - input: "foobar", - matches: ["foobar", "foobar"], - expected: { path: "foobar", index: 0, params: { name: ["foobar"] } }, - }, - { - input: "foo/bar", - matches: ["foo/bar", "foo/bar"], - expected: { - path: "foo/bar", - index: 0, - params: { name: ["foo", "bar"] }, - }, - }, - ], - }, - /** * https://github.com/pillarjs/path-to-regexp/pull/270 */ { - path: "/files/:path*.:ext*", + path: "/files{/:path}*{.:ext}*", tests: [ { input: "/files/hello/world.txt", @@ -2729,7 +2764,7 @@ const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/foo/:bar*", + path: "/foo{/:bar}*", tests: [ { input: "/foo/test1//test2", @@ -2851,7 +2886,7 @@ const MATCH_TESTS: MatchTestSet[] = [ */ { path: "/test", - options: { loose: "" }, + options: { loose: false }, tests: [ { input: "/test", @@ -2878,7 +2913,7 @@ describe("path-to-regexp", () => { const expectedKeys = [ { name: "id", - prefix: "/", + prefix: "", suffix: "", modifier: "", pattern: "", @@ -2928,7 +2963,11 @@ describe("path-to-regexp", () => { it("should throw on nested groups", () => { expect(() => { pathToRegexp.pathToRegexp("/{a{b:foo}}"); - }).toThrow(new TypeError("Unexpected { at 3, expected }")); + }).toThrow( + new TypeError( + "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", + ), + ); }); }); @@ -2992,11 +3031,11 @@ describe("path-to-regexp", () => { expect(() => { toPath({ foo: "abc" }); - }).toThrow(new TypeError('Invalid value for "foo": "/abc"')); + }).toThrow(new TypeError('Invalid value for "foo": "abc"')); }); it("should throw when expecting a repeated value", () => { - const toPath = pathToRegexp.compile("/:foo+"); + const toPath = pathToRegexp.compile("{/:foo}+"); expect(() => { toPath({ foo: [] }); @@ -3012,7 +3051,7 @@ describe("path-to-regexp", () => { }); it("should throw when a repeated param is not an array", () => { - const toPath = pathToRegexp.compile("/:foo+"); + const toPath = pathToRegexp.compile("{/:foo}+"); expect(() => { toPath({ foo: "a" }); @@ -3020,7 +3059,7 @@ describe("path-to-regexp", () => { }); it("should throw when an array value is not a string", () => { - const toPath = pathToRegexp.compile("/:foo+"); + const toPath = pathToRegexp.compile("{/:foo}+"); expect(() => { toPath({ foo: [1, "a"] }); @@ -3028,7 +3067,7 @@ describe("path-to-regexp", () => { }); it("should throw when repeated value does not match", () => { - const toPath = pathToRegexp.compile("/:foo(\\d+)+"); + const toPath = pathToRegexp.compile("{/:foo(\\d+)}+"); expect(() => { toPath({ foo: ["1", "2", "3", "a"] }); diff --git a/src/index.ts b/src/index.ts index 82961d7..b42f6db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,10 +17,6 @@ export interface ParseOptions { * Set the default delimiter for repeat parameters. (default: `'/'`) */ delimiter?: string; - /** - * List of characters to automatically consider prefixes when parsing. - */ - prefixes?: string; /** * Function for encoding input strings for output into path. */ @@ -33,9 +29,9 @@ export interface PathToRegexpOptions extends ParseOptions { */ sensitive?: boolean; /** - * Set characters to treat as "loose" and allow arbitrarily repeated. (default: `/`) + * Allow delimiter to be arbitrarily repeated. (default: `true`) */ - loose?: string; + loose?: boolean; /** * When `true` the regexp will match to the end of the string. (default: `true`) */ @@ -52,7 +48,7 @@ export interface PathToRegexpOptions extends ParseOptions { export interface MatchOptions extends PathToRegexpOptions { /** - * Function for decoding strings for params. + * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) */ decode?: Decode | false; } @@ -63,15 +59,15 @@ export interface CompileOptions extends ParseOptions { */ sensitive?: boolean; /** - * Set characters to treat as "loose" and allow arbitrarily repeated. (default: `/`) + * Allow delimiter to be arbitrarily repeated. (default: `true`) */ - loose?: string; + loose?: boolean; /** * When `false` the function can produce an invalid (unmatched) path. (default: `true`) */ validate?: boolean; /** - * Function for encoding input strings for output into the path. (default: `encodeURIComponent`) + * Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) */ encode?: Encode | false; } @@ -90,6 +86,7 @@ type TokenType = // Reserved for use. | "!" | "@" + | "," | ";"; /** @@ -105,6 +102,7 @@ const SIMPLE_TOKENS: Record = { "!": "!", "@": "@", ";": ";", + ",": ",", "*": "*", "+": "+", "?": "?", @@ -215,7 +213,9 @@ class Iter { const value = this.tryConsume(type); if (value !== undefined) return value; const { type: nextType, index } = this.peek(); - throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`); + throw new TypeError( + `Unexpected ${nextType} at ${index}, expected ${type}: https://git.new/pathToRegexpError`, + ); } text(): string { @@ -227,8 +227,10 @@ class Iter { return result; } - modifier(): string | undefined { - return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); + modifier(): string { + return ( + this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+") || "" + ); } } @@ -246,73 +248,47 @@ export class TokenData { * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): TokenData { - const { - prefixes = "./", - delimiter = DEFAULT_DELIMITER, - encodePath = NOOP_VALUE, - } = options; + const { delimiter = DEFAULT_DELIMITER, encodePath = NOOP_VALUE } = options; const tokens: Token[] = []; const it = lexer(str); let key = 0; - let path = ""; do { - const char = it.tryConsume("CHAR"); + const path = it.text(); + if (path) tokens.push(encodePath(path)); + const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); + const pattern = it.tryConsume("PATTERN") || ""; if (name || pattern) { - let prefix = char || ""; - const modifier = it.modifier(); - - if (!prefixes.includes(prefix)) { - path += prefix; - prefix = ""; - } - - if (path) { - tokens.push(encodePath(path)); - path = ""; + tokens.push({ + name: name || String(key++), + prefix: "", + suffix: "", + pattern, + modifier: "", + }); + + const next = it.peek(); + if (next.type === "*") { + throw new TypeError( + `Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: https://git.new/pathToRegexpError`, + ); } - tokens.push( - toKey( - encodePath, - delimiter, - name || String(key++), - pattern, - prefix, - "", - modifier, - ), - ); - continue; - } - - const value = char || it.tryConsume("ESCAPED"); - if (value) { - path += value; continue; } - if (path) { - tokens.push(encodePath(path)); - path = ""; - } - const asterisk = it.tryConsume("*"); if (asterisk) { - tokens.push( - toKey( - encodePath, - delimiter, - String(key++), - `[^${escape(delimiter)}]*`, - "", - "", - asterisk, - ), - ); + tokens.push({ + name: String(key++), + prefix: "", + suffix: "", + pattern: `[^${escape(delimiter)}]*`, + modifier: "*", + separator: delimiter, + }); continue; } @@ -320,22 +296,22 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (open) { const prefix = it.text(); const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); + const pattern = it.tryConsume("PATTERN") || ""; const suffix = it.text(); + const separator = it.tryConsume(";") ? it.text() : prefix + suffix; it.consume("}"); - tokens.push( - toKey( - encodePath, - delimiter, - name || (pattern ? String(key++) : ""), - pattern, - prefix, - suffix, - it.modifier(), - ), - ); + const modifier = it.modifier(); + + tokens.push({ + name: name || (pattern ? String(key++) : ""), + prefix: encodePath(prefix), + suffix: encodePath(suffix), + pattern, + modifier, + separator, + }); continue; } @@ -346,32 +322,14 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { return new TokenData(tokens, delimiter); } -function toKey( - encode: Encode, - delimiter: string, - name: string, - pattern = "", - inputPrefix = "", - inputSuffix = "", - modifier = "", -): Key { - const prefix = encode(inputPrefix); - const suffix = encode(inputSuffix); - const separator = - modifier === "*" || modifier === "+" - ? prefix + suffix || delimiter - : undefined; - return { name, prefix, suffix, pattern, modifier, separator }; -} - /** * Compile a string to a template function for the path. */ export function compile

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

(data, options); } @@ -389,10 +347,11 @@ function tokenToFunction( return () => token; } - const optional = token.modifier === "?" || token.modifier === "*"; const encodeValue = encode || NOOP_VALUE; + const repeated = token.modifier === "+" || token.modifier === "*"; + const optional = token.modifier === "?" || token.modifier === "*"; - if (encode && token.separator) { + if (encode && repeated) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}/${index}" to be a string`); @@ -456,11 +415,11 @@ function compileTokens

( ): PathFunction

{ const { encode = encodeURIComponent, - loose = DEFAULT_DELIMITER, + loose = true, validate = true, } = options; const reFlags = flags(options); - const stringify = toStringify(loose); + const stringify = toStringify(loose, data.delimiter); const keyToRegexp = toKeyRegexp(stringify, data.delimiter); // Compile all the tokens into regexps. @@ -514,26 +473,18 @@ export type MatchFunction

= (path: string) => Match

; * Create path match function from `path-to-regexp` spec. */ export function match

( - str: Path, + path: Path, options: MatchOptions = {}, ): MatchFunction

{ - const re = pathToRegexp(str, options); - return matchRegexp

(re, options); -} - -/** - * Create a path match function from `path-to-regexp` output. - */ -function matchRegexp

( - re: PathRegExp, - options: MatchOptions, -): MatchFunction

{ - const { decode = decodeURIComponent, loose = DEFAULT_DELIMITER } = options; - const stringify = toStringify(loose); + 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 decoders = re.keys.map((key) => { - if (decode && key.separator) { - const re = new RegExp(stringify(key.separator), "g"); + const decoders = keys.map((key) => { + if (decode && (key.modifier === "+" || key.modifier === "*")) { + const re = new RegExp(stringify(key.separator || ""), "g"); return (value: string) => value.split(re).map(decode); } @@ -550,7 +501,7 @@ function matchRegexp

( for (let i = 1; i < m.length; i++) { if (m[i] === undefined) continue; - const key = re.keys[i - 1]; + const key = keys[i - 1]; const decoder = decoders[i - 1]; params[key.name] = decoder(m[i]); } @@ -576,10 +527,10 @@ function looseReplacer(value: string, loose: string) { /** * Encode all non-delimiter characters using the encode function. */ -function toStringify(loose: string) { +function toStringify(loose: boolean, delimiter: string) { if (!loose) return escape; - const re = new RegExp(`[^${escape(loose)}]+|(.)`, "g"); + const re = new RegExp(`[^${escape(delimiter)}]+|(.)`, "g"); return (value: string) => value.replace(re, looseReplacer); } @@ -615,13 +566,8 @@ function tokensToRegexp( keys: Key[], options: PathToRegexpOptions, ): RegExp { - const { - trailing = true, - start = true, - end = true, - loose = DEFAULT_DELIMITER, - } = options; - const stringify = toStringify(loose); + const { trailing = true, start = true, end = true, loose = true } = options; + const stringify = toStringify(loose, data.delimiter); const keyToRegexp = toKeyRegexp(stringify, data.delimiter); let pattern = start ? "^" : ""; @@ -652,13 +598,12 @@ function toKeyRegexp(stringify: Encode, delimiter: string) { if (key.name) { const pattern = key.pattern || segmentPattern; - if (key.separator) { + if (key.modifier === "+" || key.modifier === "*") { const mod = key.modifier === "*" ? "?" : ""; - const split = stringify(key.separator); + const split = stringify(key.separator || ""); return `(?:${prefix}((?:${pattern})(?:${split}(?:${pattern}))*)${suffix})${mod}`; - } else { - return `(?:${prefix}(${pattern})${suffix})${key.modifier}`; } + return `(?:${prefix}(${pattern})${suffix})${key.modifier}`; } return `(?:${prefix}${suffix})${key.modifier}`; From 17ce0be0662afef63c277b549ba0be0112581510 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 15:42:03 -0700 Subject: [PATCH 27/55] Make key prefix/suffix/modifier/separator optional --- Readme.md | 21 +++++++++++---------- src/index.spec.ts | 25 +++++-------------------- src/index.ts | 35 +++++++++++++++-------------------- 3 files changed, 31 insertions(+), 50 deletions(-) diff --git a/Readme.md b/Readme.md index 0061eaf..801e6ef 100644 --- a/Readme.md +++ b/Readme.md @@ -242,13 +242,12 @@ A `parse` function is available and returns `TokenData`, the set of tokens and o ### Token Information -- `name` The name of the token (`string` for named or `number` for unnamed index) -- `prefix` The prefix string for the segment (e.g. `"/"`) -- `suffix` The suffix string for the segment (e.g. `""`) -- `pattern` The RegExp used to match this token (`string`) -- `modifier` The modifier character used for the segment (e.g. `?`) -- `separator` _(optional)_ The string used to separate repeated parameters (modifier is `+` or `*`) -- `optional` _(optional)_ A boolean used to indicate whether the parameter is optional (modifier is `?` or `*`) +- `name` The name of the token +- `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`) +- `suffix` _(optional)_ The suffix string for the segment (e.g. `""`) +- `pattern` _(optional)_ The pattern defined to match this token +- `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`) +- `separator` _(optional)_ The string used to separate repeated parameters ## Errors @@ -256,11 +255,13 @@ An effort has been made to ensure ambiguous paths from previous releases throw a ### Unexpected `?`, `*`, or `+` -In previous major versions, `/` or `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. +In previous major versions `/` and `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. For example: -This has been made explicit. Assuming `?` as the modifier, if you have a `/` or `.` before the parameter, you want `{.:ext}?` or `{/:ext}?`. If not, you want `{:ext}?`. +- `/:key?` → `{/:key}?` or `/:key*` → `{/:key}*` or `/:key+` → `{/:key}+` +- `.:key?` → `{.:key}?` or `.:key*` → `{.:key}*` or `.:key+` → `{.:key}+` +- `:key?` → `{:key}?` or `:key*` → `{:key}*` or `:key+` → `{:key}+` -### Unexpected `!`, `@`, or `;` +### Unexpected `!`, `@`, `,`, or `;` These characters have been reserved for future use. diff --git a/src/index.spec.ts b/src/index.spec.ts index 7f85628..3f26a97 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -36,31 +36,19 @@ const PARSER_TESTS: ParserTestSet[] = [ }, { path: "/:test", - expected: [ - "/", - { name: "test", prefix: "", suffix: "", pattern: "", modifier: "" }, - ], + expected: ["/", { name: "test" }], }, { path: "/:0", - expected: [ - "/", - { name: "0", prefix: "", suffix: "", pattern: "", modifier: "" }, - ], + expected: ["/", { name: "0" }], }, { path: "/:_", - expected: [ - "/", - { name: "_", prefix: "", suffix: "", pattern: "", modifier: "" }, - ], + expected: ["/", { name: "_" }], }, { path: "/:café", - expected: [ - "/", - { name: "café", prefix: "", suffix: "", pattern: "", modifier: "" }, - ], + expected: ["/", { name: "café" }], }, ]; @@ -2913,10 +2901,7 @@ describe("path-to-regexp", () => { const expectedKeys = [ { name: "id", - prefix: "", - suffix: "", - modifier: "", - pattern: "", + pattern: undefined, }, ]; diff --git a/src/index.ts b/src/index.ts index b42f6db..e727841 100644 --- a/src/index.ts +++ b/src/index.ts @@ -258,15 +258,12 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (path) tokens.push(encodePath(path)); const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN") || ""; + const pattern = it.tryConsume("PATTERN"); if (name || pattern) { tokens.push({ name: name || String(key++), - prefix: "", - suffix: "", pattern, - modifier: "", }); const next = it.peek(); @@ -283,8 +280,6 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - prefix: "", - suffix: "", pattern: `[^${escape(delimiter)}]*`, modifier: "*", separator: delimiter, @@ -296,7 +291,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (open) { const prefix = it.text(); const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN") || ""; + const pattern = it.tryConsume("PATTERN"); const suffix = it.text(); const separator = it.tryConsume(";") ? it.text() : prefix + suffix; @@ -350,6 +345,7 @@ function tokenToFunction( const encodeValue = encode || NOOP_VALUE; const repeated = token.modifier === "+" || token.modifier === "*"; const optional = token.modifier === "?" || token.modifier === "*"; + const { prefix = "", suffix = "", separator = "" } = token; if (encode && repeated) { const stringify = (value: string, index: number) => { @@ -366,9 +362,7 @@ function tokenToFunction( if (value.length === 0) return ""; - return ( - token.prefix + value.map(stringify).join(token.separator) + token.suffix - ); + return prefix + value.map(stringify).join(separator) + suffix; }; if (optional) { @@ -389,7 +383,7 @@ function tokenToFunction( if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}" to be a string`); } - return token.prefix + encodeValue(value) + token.suffix; + return prefix + encodeValue(value) + suffix; }; if (optional) { @@ -546,10 +540,10 @@ function flags(options: { sensitive?: boolean }) { */ export interface Key { name: string; - prefix: string; - suffix: string; - pattern: string; - modifier: string; + prefix?: string; + suffix?: string; + pattern?: string; + modifier?: string; separator?: string; } @@ -593,20 +587,21 @@ function toKeyRegexp(stringify: Encode, delimiter: string) { const segmentPattern = `[^${escape(delimiter)}]+?`; return (key: Key) => { - const prefix = stringify(key.prefix); - const suffix = stringify(key.suffix); + const prefix = key.prefix ? stringify(key.prefix) : ""; + const suffix = key.suffix ? stringify(key.suffix) : ""; + const modifier = key.modifier || ""; if (key.name) { const pattern = key.pattern || segmentPattern; if (key.modifier === "+" || key.modifier === "*") { const mod = key.modifier === "*" ? "?" : ""; - const split = stringify(key.separator || ""); + const split = key.separator ? stringify(key.separator) : ""; return `(?:${prefix}((?:${pattern})(?:${split}(?:${pattern}))*)${suffix})${mod}`; } - return `(?:${prefix}(${pattern})${suffix})${key.modifier}`; + return `(?:${prefix}(${pattern})${suffix})${modifier}`; } - return `(?:${prefix}${suffix})${key.modifier}`; + return `(?:${prefix}${suffix})${modifier}`; }; } From c0736d43dbfe531399802a70a2afdc9c0b49b174 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 16:08:48 -0700 Subject: [PATCH 28/55] Remove loose string reference --- Readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 801e6ef..9acfa82 100644 --- a/Readme.md +++ b/Readme.md @@ -166,11 +166,11 @@ regexp.exec("/bar/baz"); #### Wildcard -A wildcard can also be used. It is roughly equivalent to `(.*)` except when decoding in `match` below it splits on the delimiter. +A wildcard can also be used. It is roughly equivalent to `(.*)`. ```js const regexp = pathToRegexp("/*"); -// keys = [{ name: '0', pattern: '[^\\/]*', modifier: '*' }] +// keys = [{ name: '0', pattern: '[^\\/]*', separator: '/', modifier: '*' }] regexp.exec("/"); //=> [ '/', '', index: 0 ] @@ -234,7 +234,7 @@ toPathRegexp({ id: "123" }); //=> "/user/123" - If you are rewriting paths with match and compiler, 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: ''`, `trailing: false`, and `sensitive: true`. +- If matches are intended to be exact, you need to set `loose: false`, `trailing: false`, and `sensitive: true`. ### Parse From c1541bcf264529588849badc4c9c291e26b39db9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 16:09:33 -0700 Subject: [PATCH 29/55] NPM audit fix --- package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2eddca6..caab9a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1194,12 +1194,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1585,9 +1585,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4392,12 +4392,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "bytes-iec": { @@ -4690,9 +4690,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" From ec35fbd500a08a7b06e45f2e23dae4b0a3690a54 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 16:10:34 -0700 Subject: [PATCH 30/55] 7.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index caab9a9..c58c574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "path-to-regexp", - "version": "6.2.2", + "version": "7.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "path-to-regexp", - "version": "6.2.2", + "version": "7.0.0", "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", diff --git a/package.json b/package.json index ed066c7..e09f720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "path-to-regexp", - "version": "6.2.2", + "version": "7.0.0", "description": "Express style path to RegExp utility", "keywords": [ "express", From 140b8248e99c34414910d0d2d6c0e822327e1197 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 20 Jun 2024 17:58:03 -0700 Subject: [PATCH 31/55] Document ; and TokenData better --- Readme.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 9acfa82..4aa29b4 100644 --- a/Readme.md +++ b/Readme.md @@ -164,6 +164,20 @@ regexp.exec("/bar/baz"); //=> [ '/bar/baz', 'bar/baz', index: 0 ] ``` +##### Custom separator + +By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: + +```js +const regexp = pathToRegexp("/name{/:parts;-}+"); + +regexp.exec("/name"); +//=> null + +regexp.exec("/bar/1-2-3"); +//=> [ '/name/1-2-3', '1-2-3', index: 0 ] +``` + #### Wildcard A wildcard can also be used. It is roughly equivalent to `(.*)`. @@ -240,7 +254,9 @@ toPathRegexp({ id: "123" }); //=> "/user/123" 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. -### Token Information +### Tokens + +The `tokens` returned by `TokenData` is an array of strings or keys, represented as objects, with the following properties: - `name` The name of the token - `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`) @@ -249,6 +265,20 @@ A `parse` function is available and returns `TokenData`, the set of tokens and o - `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`) - `separator` _(optional)_ The string used to separate repeated parameters +### 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: + +```js +import { TokenData, match } from "path-to-regexp"; + +const tokens = ["/", { name: "foo" }]; +const path = new TokenData(tokens, "/"); +const fn = match(path); + +fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } +``` + ## Errors An effort has been made to ensure ambiguous paths from previous releases throw an error. This means you might be seeing an error when things worked before. From c9589365d0aba33b9745ebb9a909efdc50750007 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sat, 13 Jul 2024 15:44:17 -0700 Subject: [PATCH 32/55] Clarify match/compile examples in README --- Readme.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Readme.md b/Readme.md index 4aa29b4..8a25609 100644 --- a/Readme.md +++ b/Readme.md @@ -202,8 +202,7 @@ The `match` function returns a function for transforming paths into parameters: - **decode** Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) ```js -// Make sure you consistently `decode` segments. -const fn = match("/user/:id", { decode: decodeURIComponent }); +const fn = match("/user/:id"); fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } } fn("/invalid"); //=> false @@ -224,15 +223,14 @@ The `compile` function will return a function for transforming parameters into a ```js const toPath = compile("/user/:id"); -toPath({ id: 123 }); //=> "/user/123" +toPath({ id: "name" }); //=> "/user/name" toPath({ id: "café" }); //=> "/user/caf%C3%A9" -toPath({ id: ":/" }); //=> "/user/%3A%2F" // When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. const toPathRaw = compile("/user/:id", { encode: false }); toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" -toPathRaw({ id: ":/" }); //=> "/user/:/", throws when `validate: false` is not set. +toPathRaw({ id: ":/" }); //=> Throws, "/user/:/" when `validate` is `false`. const toPathRepeated = compile("{/:segment}+"); From eaed1fc3aa682be1562efcb2f7b738c13dfae20e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sat, 13 Jul 2024 15:44:41 -0700 Subject: [PATCH 33/55] Document unexpected ; in errors --- Readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 8a25609..40c4795 100644 --- a/Readme.md +++ b/Readme.md @@ -289,7 +289,11 @@ In previous major versions `/` and `.` were used as implicit prefixes of paramet - `.:key?` → `{.:key}?` or `.:key*` → `{.:key}*` or `.:key+` → `{.:key}+` - `:key?` → `{:key}?` or `:key*` → `{:key}*` or `:key+` → `{:key}+` -### Unexpected `!`, `@`, `,`, or `;` +### Unexpected `;` + +Used as a [custom separator](#custom-separator) for repeated parameters. + +### Unexpected `!`, `@`, or `,` These characters have been reserved for future use. From f73ec6c86b06f544b977119c2b62a16de480a6a9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sat, 13 Jul 2024 16:14:22 -0700 Subject: [PATCH 34/55] Adds strict mode, redos script, improved separator and delimiter --- Readme.md | 7 +- package-lock.json | 116 +- package.json | 3 +- scripts/redos.ts | 36 + src/cases.spec.ts | 3194 +++++++++++++++++++++++++++++++++++++++++++++ src/index.spec.ts | 2984 +----------------------------------------- src/index.ts | 200 ++- 7 files changed, 3527 insertions(+), 3013 deletions(-) create mode 100644 scripts/redos.ts create mode 100644 src/cases.spec.ts diff --git a/Readme.md b/Readme.md index 40c4795..66cbc29 100644 --- a/Readme.md +++ b/Readme.md @@ -32,12 +32,12 @@ The `pathToRegexp` function returns a regular expression with `keys` as a proper - **path** A string. - **options** _(optional)_ - **sensitive** Regexp will be case sensitive. (default: `false`) - - **trailing** Regexp allows an optional trailing delimiter to match. (default: `true`) + - **trailing** Allows optional trailing delimiter to match. (default: `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 repeated an arbitrary number of times. (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 to encode strings before inserting into `RegExp`. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) + - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) ```js const regexp = pathToRegexp("/foo/:bar"); @@ -247,6 +247,7 @@ toPathRegexp({ id: "123" }); //=> "/user/123" - If you are rewriting paths with match and compiler, 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 diff --git a/package-lock.json b/package-lock.json index c58c574..4ce4895 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,9 @@ "@types/node": "^20.4.9", "@types/semver": "^7.3.1", "@vitest/coverage-v8": "^1.4.0", + "recheck": "^4.4.5", "size-limit": "^11.1.2", - "typescript": "^5.1.6" + "typescript": "^5.5.3" }, "engines": { "node": ">=16" @@ -2684,6 +2685,67 @@ "node": ">=8.10.0" } }, + "node_modules/recheck": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck/-/recheck-4.4.5.tgz", + "integrity": "sha512-J80Ykhr+xxWtvWrfZfPpOR/iw2ijvb4WY8d9AVoN8oHsPP07JT1rCAalUSACMGxM1cvSocb6jppWFjVS6eTTrA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "recheck-jar": "4.4.5", + "recheck-linux-x64": "4.4.5", + "recheck-macos-x64": "4.4.5", + "recheck-windows-x64": "4.4.5" + } + }, + "node_modules/recheck-jar": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-jar/-/recheck-jar-4.4.5.tgz", + "integrity": "sha512-a2kMzcfr+ntT0bObNLY22EUNV6Z6WeZ+DybRmPOUXVWzGcqhRcrK74tpgrYt3FdzTlSh85pqoryAPmrNkwLc0g==", + "dev": true, + "optional": true + }, + "node_modules/recheck-linux-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-linux-x64/-/recheck-linux-x64-4.4.5.tgz", + "integrity": "sha512-s8OVPCpiSGw+tLCxH3eei7Zp2AoL22kXqLmEtWXi0AnYNwfuTjZmeLn2aQjW8qhs8ZPSkxS7uRIRTeZqR5Fv/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/recheck-macos-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-macos-x64/-/recheck-macos-x64-4.4.5.tgz", + "integrity": "sha512-Ouup9JwwoKCDclt3Na8+/W2pVbt8FRpzjkDuyM32qTR2TOid1NI+P1GA6/VQAKEOjvaxgGjxhcP/WqAjN+EULA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/recheck-windows-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-windows-x64/-/recheck-windows-x64-4.4.5.tgz", + "integrity": "sha512-mkpzLHu9G9Ztjx8HssJh9k/Xm1d1d/4OoT7etHqFk+k1NGzISCRXBD22DqYF9w8+J4QEzTAoDf8icFt0IGhOEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -3307,9 +3369,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -5424,6 +5486,46 @@ "picomatch": "^2.2.1" } }, + "recheck": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck/-/recheck-4.4.5.tgz", + "integrity": "sha512-J80Ykhr+xxWtvWrfZfPpOR/iw2ijvb4WY8d9AVoN8oHsPP07JT1rCAalUSACMGxM1cvSocb6jppWFjVS6eTTrA==", + "dev": true, + "requires": { + "recheck-jar": "4.4.5", + "recheck-linux-x64": "4.4.5", + "recheck-macos-x64": "4.4.5", + "recheck-windows-x64": "4.4.5" + } + }, + "recheck-jar": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-jar/-/recheck-jar-4.4.5.tgz", + "integrity": "sha512-a2kMzcfr+ntT0bObNLY22EUNV6Z6WeZ+DybRmPOUXVWzGcqhRcrK74tpgrYt3FdzTlSh85pqoryAPmrNkwLc0g==", + "dev": true, + "optional": true + }, + "recheck-linux-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-linux-x64/-/recheck-linux-x64-4.4.5.tgz", + "integrity": "sha512-s8OVPCpiSGw+tLCxH3eei7Zp2AoL22kXqLmEtWXi0AnYNwfuTjZmeLn2aQjW8qhs8ZPSkxS7uRIRTeZqR5Fv/Q==", + "dev": true, + "optional": true + }, + "recheck-macos-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-macos-x64/-/recheck-macos-x64-4.4.5.tgz", + "integrity": "sha512-Ouup9JwwoKCDclt3Na8+/W2pVbt8FRpzjkDuyM32qTR2TOid1NI+P1GA6/VQAKEOjvaxgGjxhcP/WqAjN+EULA==", + "dev": true, + "optional": true + }, + "recheck-windows-x64": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/recheck-windows-x64/-/recheck-windows-x64-4.4.5.tgz", + "integrity": "sha512-mkpzLHu9G9Ztjx8HssJh9k/Xm1d1d/4OoT7etHqFk+k1NGzISCRXBD22DqYF9w8+J4QEzTAoDf8icFt0IGhOEQ==", + "dev": true, + "optional": true + }, "restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -5862,9 +5964,9 @@ "peer": true }, "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true }, "ufo": { diff --git a/package.json b/package.json index e09f720..e99e651 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,9 @@ "@types/node": "^20.4.9", "@types/semver": "^7.3.1", "@vitest/coverage-v8": "^1.4.0", + "recheck": "^4.4.5", "size-limit": "^11.1.2", - "typescript": "^5.1.6" + "typescript": "^5.5.3" }, "engines": { "node": ">=16" diff --git a/scripts/redos.ts b/scripts/redos.ts new file mode 100644 index 0000000..c675e71 --- /dev/null +++ b/scripts/redos.ts @@ -0,0 +1,36 @@ +import { checkSync } from "recheck"; +import { pathToRegexp } from "../src/index.js"; + +const TESTS = [ + "/abc{abc:foo}?", + "/:foo{abc:foo}?", + "{:attr1}?{:attr2/}?", + "{:attr1/}?{:attr2/}?", + "{:foo.}?{:bar.}?", + "{:foo([^\\.]+).}?{:bar.}?", + ":foo(a+):bar(b+)", +]; + +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); + } + } +} diff --git a/src/cases.spec.ts b/src/cases.spec.ts new file mode 100644 index 0000000..23a1814 --- /dev/null +++ b/src/cases.spec.ts @@ -0,0 +1,3194 @@ +import type { + Path, + MatchOptions, + Match, + ParseOptions, + Token, + CompileOptions, + ParamData, +} from "./index.js"; + +export interface ParserTestSet { + path: string; + options?: ParseOptions; + expected: Token[]; +} + +export interface CompileTestSet { + path: string; + options?: CompileOptions; + tests: Array<{ + input: ParamData | undefined; + expected: string | null; + }>; +} + +export interface MatchTestSet { + path: Path; + options?: MatchOptions; + tests: Array<{ + input: string; + matches: (string | undefined)[] | null; + expected: Match; + }>; +} + +export const PARSER_TESTS: ParserTestSet[] = [ + { + path: "/", + expected: ["/"], + }, + { + path: "/:test", + expected: ["/", { name: "test" }], + }, + { + path: "/:0", + expected: ["/", { name: "0" }], + }, + { + path: "/:_", + expected: ["/", { name: "_" }], + }, + { + path: "/:café", + expected: ["/", { name: "café" }], + }, +]; + +export const COMPILE_TESTS: CompileTestSet[] = [ + { + path: "/", + tests: [ + { input: undefined, expected: "/" }, + { input: {}, expected: "/" }, + { input: { id: "123" }, expected: "/" }, + ], + }, + { + path: "/test", + tests: [ + { input: undefined, expected: "/test" }, + { input: {}, expected: "/test" }, + { input: { id: "123" }, expected: "/test" }, + ], + }, + { + path: "/test/", + tests: [ + { input: undefined, expected: "/test/" }, + { input: {}, expected: "/test/" }, + { input: { id: "123" }, expected: "/test/" }, + ], + }, + { + path: "/:0", + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { 0: "123" }, expected: "/123" }, + ], + }, + { + path: "/:test", + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, + ], + }, + { + path: "/:test", + options: { validate: false }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, + ], + }, + { + path: "/:test", + options: { validate: false, encode: false }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + ], + }, + { + path: "/:test", + options: { encode: encodeURIComponent }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, + ], + }, + { + path: "/:test", + options: { encode: () => "static" }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "123" }, expected: "/static" }, + { input: { test: "123/xyz" }, expected: "/static" }, + ], + }, + { + path: "{/:test}?", + options: { encode: false }, + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: undefined }, expected: "" }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: null }, + ], + }, + { + path: "/:test(.*)", + options: { encode: false }, + tests: [ + { input: undefined, expected: null }, + { input: {}, expected: null }, + { input: { test: "" }, expected: "/" }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + ], + }, + { + path: "{/:test}*", + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: [] }, expected: "" }, + { input: { test: [""] }, expected: null }, + { input: { test: ["123"] }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: null }, + { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, + ], + }, + { + path: "{/:test}*", + options: { encode: false }, + tests: [ + { input: undefined, expected: "" }, + { input: {}, expected: "" }, + { input: { test: "" }, expected: null }, + { input: { test: "123" }, expected: "/123" }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, + { input: { test: ["123", "xyz"] }, expected: null }, + ], + }, + { + path: "/{<:foo>}+", + tests: [ + { input: undefined, expected: null }, + { input: { foo: ["x", "y", "z"] }, expected: "/" }, + ], + }, +]; + +/** + * An array of test cases with expected inputs and outputs. + */ +export const MATCH_TESTS: MatchTestSet[] = [ + /** + * Simple paths. + */ + { + path: "/", + tests: [ + { + input: "/", + matches: ["/"], + expected: { path: "/", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + ], + }, + { + path: "/test", + tests: [ + { + input: "/test", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + { input: "/test/route", matches: null, expected: false }, + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", index: 0, params: {} }, + }, + ], + }, + { + path: "/test/", + tests: [ + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", index: 0, params: {} }, + }, + { input: "/route", matches: null, expected: false }, + { input: "/test", matches: null, expected: false }, + { + input: "/test//", + matches: ["/test//"], + expected: { path: "/test//", index: 0, params: {} }, + }, + ], + }, + { + path: "/:test", + 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: null, + 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é" }, + }, + }, + { + input: "/;,:@&=+$-_.!~*()", + matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], + expected: { + path: "/;,:@&=+$-_.!~*()", + index: 0, + params: { test: ";,:@&=+$-_.!~*()" }, + }, + }, + ], + }, + + /** + * 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, + }, + { + input: "/route/test//", + matches: null, + expected: false, + }, + { + input: "/route//test", + matches: null, + expected: false, + }, + ], + }, + + /** + * Non-starting and non-ending modes. + */ + { + path: "/test", + options: { + start: false, + 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/route", + matches: ["/test"], + expected: { path: "/test", index: 0, params: {} }, + }, + { + input: "/route/test", + matches: ["/test"], + expected: { path: "/test", index: 6, params: {} }, + }, + ], + }, + { + 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//", index: 0, params: {} }, + }, + { + input: "/test/route", + matches: null, + expected: false, + }, + { + input: "/route/test/deep", + 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" } }, + }, + { + 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: { + start: false, + 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: ["/test/", "test"], + expected: { path: "/test/", index: 6, params: { test: "test" } }, + }, + { + input: "/route/test//", + matches: ["/test//", "test"], + expected: { path: "/test//", index: 6, params: { test: "test" } }, + }, + ], + }, + + /** + * Optional. + */ + { + path: "{/:test}?", + 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///", + matches: ["///route///", "route"], + expected: { path: "///route///", index: 0, params: { test: "route" } }, + }, + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "///", + matches: ["///", undefined], + expected: { path: "///", 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: "/", matches: null, expected: false }, + { input: "///", matches: null, expected: false }, + ], + }, + { + path: "{/:test}?/bar", + tests: [ + { + input: "/bar", + matches: ["/bar", undefined], + expected: { path: "/bar", index: 0, 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" } }, + }, + { + input: "/foo/bar/", + matches: ["/foo/bar/", "foo"], + expected: { path: "/foo/bar/", index: 0, params: { test: "foo" } }, + }, + ], + }, + { + path: "{/:test}?-bar", + tests: [ + { + input: "-bar", + matches: ["-bar", undefined], + expected: { path: "-bar", index: 0, 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" } }, + }, + ], + }, + { + path: "/{:test}?-bar", + tests: [ + { + input: "/-bar", + matches: ["/-bar", undefined], + expected: { path: "/-bar", index: 0, 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" } }, + }, + ], + }, + + /** + * Zero or more times. + */ + { + path: "{/:test}*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "//", + matches: ["//", undefined], + expected: { path: "//", index: 0, params: {} }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, 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"] }, + }, + }, + ], + }, + { + path: "{/:test}*-bar", + tests: [ + { + input: "-bar", + matches: ["-bar", undefined], + expected: { path: "-bar", index: 0, params: {} }, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/foo-bar", + matches: ["/foo-bar", "foo"], + expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + }, + { + input: "/foo/baz-bar", + matches: ["/foo/baz-bar", "foo/baz"], + expected: { + path: "/foo/baz-bar", + index: 0, + params: { test: ["foo", "baz"] }, + }, + }, + ], + }, + + /** + * One or more times. + */ + { + path: "{/:test}+", + tests: [ + { + input: "/", + matches: null, + expected: false, + }, + { + input: "//", + matches: null, + expected: false, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, 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"] }, + }, + }, + ], + }, + { + path: "{/:test}+-bar", + tests: [ + { + input: "-bar", + matches: null, + expected: false, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/foo-bar", + matches: ["/foo-bar", "foo"], + expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + }, + { + input: "/foo/baz-bar", + matches: ["/foo/baz-bar", "foo/baz"], + expected: { + path: "/foo/baz-bar", + index: 0, + params: { test: ["foo", "baz"] }, + }, + }, + ], + }, + + /** + * Custom parameters. + */ + { + path: String.raw`/:test(\d+)`, + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { test: "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, + ], + }, + { + path: String.raw`/:test(\d+)-bar`, + tests: [ + { + input: "-bar", + matches: null, + expected: false, + }, + { + input: "/-bar", + matches: null, + expected: false, + }, + { + input: "/abc-bar", + matches: null, + expected: false, + }, + { + input: "/123-bar", + matches: ["/123-bar", "123"], + expected: { path: "/123-bar", index: 0, params: { test: "123" } }, + }, + { + input: "/123/456-bar", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test(.*)", + tests: [ + { + input: "/", + matches: ["/", ""], + expected: { path: "/", index: 0, params: { test: "" } }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { test: "route" } }, + }, + { + input: "/route/123", + matches: ["/route/123", "route/123"], + expected: { + path: "/route/123", + index: 0, + params: { test: "route/123" }, + }, + }, + { + input: "/;,:@&=/+$-_.!/~*()", + matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], + expected: { + path: "/;,:@&=/+$-_.!/~*()", + index: 0, + params: { test: ";,:@&=/+$-_.!/~*()" }, + }, + }, + ], + }, + { + path: "/:test([a-z]+)", + tests: [ + { + input: "/abc", + matches: ["/abc", "abc"], + expected: { path: "/abc", index: 0, params: { test: "abc" } }, + }, + { + input: "/123", + matches: null, + expected: false, + }, + { + input: "/abc/123", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test(this|that)", + tests: [ + { + input: "/this", + matches: ["/this", "this"], + expected: { path: "/this", index: 0, params: { test: "this" } }, + }, + { + input: "/that", + matches: ["/that", "that"], + expected: { path: "/that", index: 0, params: { test: "that" } }, + }, + { + input: "/foo", + matches: null, + expected: false, + }, + ], + }, + { + path: "{/:test(abc|xyz)}*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { test: undefined } }, + }, + { + input: "/abc", + matches: ["/abc", "abc"], + expected: { path: "/abc", index: 0, params: { test: ["abc"] } }, + }, + { + input: "/abc/abc", + matches: ["/abc/abc", "abc/abc"], + expected: { + path: "/abc/abc", + index: 0, + params: { test: ["abc", "abc"] }, + }, + }, + { + input: "/xyz/xyz", + matches: ["/xyz/xyz", "xyz/xyz"], + expected: { + path: "/xyz/xyz", + index: 0, + params: { test: ["xyz", "xyz"] }, + }, + }, + { + input: "/abc/xyz", + matches: ["/abc/xyz", "abc/xyz"], + expected: { + path: "/abc/xyz", + index: 0, + params: { test: ["abc", "xyz"] }, + }, + }, + { + input: "/abc/xyz/abc/xyz", + 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, + expected: false, + }, + ], + }, + + /** + * No prefix characters. + */ + { + path: "test", + tests: [ + { + input: "test", + matches: ["test"], + expected: { path: "test", index: 0, params: {} }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: ":test", + tests: [ + { + input: "route", + matches: ["route", "route"], + expected: { path: "route", index: 0, params: { test: "route" } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "route/", + matches: ["route/", "route"], + expected: { path: "route/", index: 0, params: { test: "route" } }, + }, + ], + }, + { + path: "{:test}?", + tests: [ + { + input: "test", + matches: ["test", "test"], + expected: { path: "test", index: 0, params: { test: "test" } }, + }, + { + input: "", + matches: ["", undefined], + expected: { path: "", index: 0, params: {} }, + }, + ], + }, + { + path: "{:test/}+", + tests: [ + { + input: "route/", + matches: ["route/", "route"], + expected: { path: "route/", index: 0, params: { test: ["route"] } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "", + matches: null, + expected: false, + }, + { + input: "foo/bar/", + matches: ["foo/bar/", "foo/bar"], + expected: { + path: "foo/bar/", + index: 0, + params: { test: ["foo", "bar"] }, + }, + }, + ], + }, + + /** + * Formats. + */ + { + path: "/test.json", + tests: [ + { + input: "/test.json", + matches: ["/test.json"], + expected: { path: "/test.json", index: 0, params: {} }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test.json", + tests: [ + { + input: "/.json", + matches: null, + expected: false, + }, + { + input: "/test.json", + matches: ["/test.json", "test"], + expected: { path: "/test.json", index: 0, params: { test: "test" } }, + }, + { + input: "/route.json", + matches: ["/route.json", "route"], + expected: { path: "/route.json", index: 0, params: { test: "route" } }, + }, + { + input: "/route.json.json", + matches: ["/route.json.json", "route.json"], + expected: { + path: "/route.json.json", + index: 0, + params: { test: "route.json" }, + }, + }, + ], + }, + + /** + * Format params. + */ + { + path: "/test.:format(\\w+)", + tests: [ + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { path: "/test.html", index: 0, params: { format: "html" } }, + }, + { + input: "/test", + matches: null, + expected: false, + }, + ], + }, + { + path: "/test.:format(\\w+).:format(\\w+)", + tests: [ + { + input: "/test.html.json", + matches: ["/test.html.json", "html", "json"], + expected: { + path: "/test.html.json", + index: 0, + params: { format: "json" }, + }, + }, + { + input: "/test.html", + matches: null, + expected: false, + }, + ], + }, + { + path: "/test{.:format(\\w+)}?", + tests: [ + { + input: "/test", + matches: ["/test", undefined], + expected: { path: "/test", index: 0, params: { format: undefined } }, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { path: "/test.html", index: 0, params: { format: "html" } }, + }, + ], + }, + { + path: "/test{.:format(\\w+)}+", + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { + path: "/test.html", + index: 0, + params: { format: ["html"] }, + }, + }, + { + input: "/test.html.json", + matches: ["/test.html.json", "html.json"], + expected: { + path: "/test.html.json", + index: 0, + params: { format: ["html", "json"] }, + }, + }, + ], + }, + { + path: "/test{.:format}+", + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test.html", + matches: ["/test.html", "html"], + expected: { + path: "/test.html", + index: 0, + params: { format: ["html"] }, + }, + }, + { + input: "/test.hbs.html", + matches: ["/test.hbs.html", "hbs.html"], + expected: { + path: "/test.hbs.html", + index: 0, + params: { format: ["hbs", "html"] }, + }, + }, + ], + }, + + /** + * Format and path params. + */ + { + path: "/:test.:format", + tests: [ + { + input: "/route.html", + matches: ["/route.html", "route", "html"], + expected: { + path: "/route.html", + index: 0, + params: { test: "route", format: "html" }, + }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + { + input: "/route.html.json", + matches: ["/route.html.json", "route", "html.json"], + expected: { + path: "/route.html.json", + index: 0, + params: { test: "route", format: "html.json" }, + }, + }, + ], + }, + { + path: "/:test{.:format}?", + tests: [ + { + input: "/route", + matches: ["/route", "route", undefined], + 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", format: "json" }, + }, + }, + { + input: "/route.json.html", + matches: ["/route.json.html", "route", "json.html"], + expected: { + path: "/route.json.html", + index: 0, + params: { test: "route", format: "json.html" }, + }, + }, + ], + }, + { + path: "/:test.:format\\z", + tests: [ + { + input: "/route.htmlz", + matches: ["/route.htmlz", "route", "html"], + expected: { + path: "/route.htmlz", + index: 0, + params: { test: "route", format: "html" }, + }, + }, + { + input: "/route.html", + matches: null, + expected: false, + }, + ], + }, + + /** + * Unnamed params. + */ + { + path: "/(\\d+)", + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { "0": "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, + ], + }, + { + path: "{/(\\d+)}?", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { "0": undefined } }, + }, + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { "0": "123" } }, + }, + ], + }, + { + path: "/route\\(\\\\(\\d+\\\\)\\)", + tests: [ + { + input: "/route(\\123\\)", + matches: ["/route(\\123\\)", "123\\"], + expected: { + path: "/route(\\123\\)", + index: 0, + params: { "0": "123\\" }, + }, + }, + { + input: "/route(\\123)", + matches: null, + expected: false, + }, + ], + }, + { + path: "{/route}?", + tests: [ + { + input: "", + matches: [""], + expected: { path: "", index: 0, params: {} }, + }, + { + input: "/", + matches: ["/"], + expected: { path: "/", index: 0, params: {} }, + }, + { + input: "/foo", + matches: null, + expected: false, + }, + { + input: "/route", + matches: ["/route"], + expected: { path: "/route", index: 0, params: {} }, + }, + ], + }, + { + path: "{/(.*)}", + tests: [ + { + input: "/", + matches: ["/", ""], + expected: { path: "/", index: 0, params: { "0": "" } }, + }, + { + input: "/login", + matches: ["/login", "login"], + expected: { path: "/login", index: 0, params: { "0": "login" } }, + }, + ], + }, + + /** + * Escaped characters. + */ + { + path: "/\\(testing\\)", + tests: [ + { + input: "/testing", + matches: null, + expected: false, + }, + { + input: "/(testing)", + matches: ["/(testing)"], + expected: { path: "/(testing)", index: 0, params: {} }, + }, + ], + }, + { + path: "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", + tests: [ + { + input: "/.+*?{}=^!:$[]|", + matches: ["/.+*?{}=^!:$[]|"], + expected: { path: "/.+*?{}=^!:$[]|", index: 0, params: {} }, + }, + ], + }, + { + path: "/test/{:uid(u\\d+)}?{:cid(c\\d+)}?", + tests: [ + { + input: "/test/u123", + matches: ["/test/u123", "u123", undefined], + expected: { path: "/test/u123", index: 0, params: { uid: "u123" } }, + }, + { + input: "/test/c123", + matches: ["/test/c123", undefined, "c123"], + expected: { path: "/test/c123", index: 0, params: { cid: "c123" } }, + }, + ], + }, + + /** + * Unnamed group prefix. + */ + { + path: "/{apple-}?icon-:res(\\d+).png", + tests: [ + { + input: "/icon-240.png", + matches: ["/icon-240.png", "240"], + expected: { path: "/icon-240.png", index: 0, 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" }, + }, + }, + ], + }, + + /** + * Random examples. + */ + { + path: "/:foo/:bar", + tests: [ + { + input: "/match/route", + matches: ["/match/route", "match", "route"], + expected: { + path: "/match/route", + index: 0, + params: { foo: "match", bar: "route" }, + }, + }, + ], + }, + { + path: "/:foo\\(test\\)/bar", + tests: [ + { + input: "/foo(test)/bar", + matches: ["/foo(test)/bar", "foo"], + expected: { path: "/foo(test)/bar", index: 0, params: { foo: "foo" } }, + }, + { + input: "/foo/bar", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:remote([\\w\\-\\.]+)/:user([\\w-]+)", + tests: [ + { + input: "/endpoint/user", + matches: ["/endpoint/user", "endpoint", "user"], + expected: { + path: "/endpoint/user", + index: 0, + params: { remote: "endpoint", user: "user" }, + }, + }, + { + input: "/endpoint/user-name", + matches: ["/endpoint/user-name", "endpoint", "user-name"], + expected: { + path: "/endpoint/user-name", + index: 0, + params: { remote: "endpoint", user: "user-name" }, + }, + }, + { + input: "/foo.bar/user-name", + matches: ["/foo.bar/user-name", "foo.bar", "user-name"], + expected: { + path: "/foo.bar/user-name", + index: 0, + params: { remote: "foo.bar", user: "user-name" }, + }, + }, + ], + }, + { + path: "/:foo\\?", + tests: [ + { + input: "/route?", + matches: ["/route?", "route"], + expected: { path: "/route?", index: 0, params: { foo: "route" } }, + }, + { + input: "/route", + matches: null, + expected: false, + }, + ], + }, + { + path: "{/:foo}+bar", + tests: [ + { + input: "/foobar", + matches: ["/foobar", "foo"], + expected: { path: "/foobar", index: 0, params: { foo: ["foo"] } }, + }, + { + input: "/foo/bar", + matches: null, + expected: false, + }, + { + input: "/foo/barbar", + matches: ["/foo/barbar", "foo/bar"], + expected: { + path: "/foo/barbar", + index: 0, + params: { foo: ["foo", "bar"] }, + }, + }, + ], + }, + { + path: "/{:pre}?baz", + tests: [ + { + input: "/foobaz", + matches: ["/foobaz", "foo"], + expected: { path: "/foobaz", index: 0, params: { pre: "foo" } }, + }, + { + input: "/baz", + matches: ["/baz", undefined], + expected: { path: "/baz", index: 0, params: { pre: undefined } }, + }, + ], + }, + { + path: "/:foo\\(:bar\\)", + tests: [ + { + input: "/hello(world)", + matches: ["/hello(world)", "hello", "world"], + expected: { + path: "/hello(world)", + index: 0, + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello()", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:foo\\({:bar}?\\)", + tests: [ + { + input: "/hello(world)", + matches: ["/hello(world)", "hello", "world"], + expected: { + path: "/hello(world)", + index: 0, + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello()", + matches: ["/hello()", "hello", undefined], + expected: { + path: "/hello()", + index: 0, + params: { foo: "hello", bar: undefined }, + }, + }, + ], + }, + { + path: "/:postType(video|audio|text){(\\+.+)}?", + tests: [ + { + input: "/video", + matches: ["/video", "video", undefined], + expected: { path: "/video", index: 0, 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, + expected: false, + }, + ], + }, + { + path: "{/:foo}?{/:bar}?-ext", + tests: [ + { + input: "/-ext", + matches: null, + expected: false, + }, + { + input: "-ext", + 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" } }, + }, + { + input: "/foo/bar-ext", + matches: ["/foo/bar-ext", "foo", "bar"], + expected: { + path: "/foo/bar-ext", + index: 0, + params: { foo: "foo", bar: "bar" }, + }, + }, + { + input: "/foo/-ext", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:required{/:optional}?-ext", + tests: [ + { + input: "/foo-ext", + matches: ["/foo-ext", "foo", undefined], + expected: { path: "/foo-ext", index: 0, 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" }, + }, + }, + { + input: "/foo/-ext", + matches: null, + expected: false, + }, + ], + }, + + /** + * Unicode matches. + */ + { + path: "/:foo", + tests: [ + { + input: "/café", + matches: ["/café", "café"], + expected: { path: "/café", index: 0, params: { foo: "café" } }, + }, + ], + }, + { + path: "/:foo", + options: { + decode: false, + }, + tests: [ + { + input: "/caf%C3%A9", + matches: ["/caf%C3%A9", "caf%C3%A9"], + expected: { + path: "/caf%C3%A9", + index: 0, + params: { foo: "caf%C3%A9" }, + }, + }, + ], + }, + { + path: "/café", + tests: [ + { + input: "/café", + matches: ["/café"], + expected: { path: "/café", index: 0, params: {} }, + }, + ], + }, + { + path: "/café", + options: { + encodePath: encodeURI, + }, + tests: [ + { + input: "/caf%C3%A9", + matches: ["/caf%C3%A9"], + expected: { path: "/caf%C3%A9", index: 0, params: {} }, + }, + ], + }, + + /** + * Hostnames. + */ + { + path: ":domain.com", + options: { + delimiter: ".", + }, + tests: [ + { + input: "example.com", + matches: ["example.com", "example"], + expected: { + path: "example.com", + index: 0, + params: { domain: "example" }, + }, + }, + { + input: "github.com", + matches: ["github.com", "github"], + expected: { + path: "github.com", + index: 0, + params: { domain: "github" }, + }, + }, + ], + }, + { + path: "mail.:domain.com", + options: { + delimiter: ".", + }, + tests: [ + { + input: "mail.example.com", + matches: ["mail.example.com", "example"], + expected: { + path: "mail.example.com", + index: 0, + params: { domain: "example" }, + }, + }, + { + input: "mail.github.com", + matches: ["mail.github.com", "github"], + expected: { + path: "mail.github.com", + index: 0, + params: { domain: "github" }, + }, + }, + ], + }, + { + path: "mail{.:domain}?.com", + options: { + delimiter: ".", + }, + tests: [ + { + input: "mail.com", + matches: ["mail.com", undefined], + expected: { path: "mail.com", index: 0, params: { domain: undefined } }, + }, + { + input: "mail.example.com", + matches: ["mail.example.com", "example"], + expected: { + path: "mail.example.com", + index: 0, + params: { domain: "example" }, + }, + }, + { + input: "mail.github.com", + matches: ["mail.github.com", "github"], + expected: { + path: "mail.github.com", + index: 0, + params: { domain: "github" }, + }, + }, + ], + }, + { + path: "example.:ext", + options: { + delimiter: ".", + }, + tests: [ + { + input: "example.com", + matches: ["example.com", "com"], + expected: { path: "example.com", index: 0, params: { ext: "com" } }, + }, + { + input: "example.org", + matches: ["example.org", "org"], + expected: { path: "example.org", index: 0, params: { ext: "org" } }, + }, + ], + }, + { + path: "this is", + options: { + delimiter: " ", + end: false, + }, + tests: [ + { + input: "this is a test", + matches: ["this is"], + expected: { path: "this is", index: 0, params: {} }, + }, + { + input: "this isn't", + matches: null, + expected: false, + }, + ], + }, + + /** + * Prefixes. + */ + { + path: "{$:foo}{$:bar}?", + tests: [ + { + input: "$x", + matches: ["$x", "x", undefined], + expected: { path: "$x", index: 0, params: { foo: "x" } }, + }, + { + input: "$x$y", + matches: ["$x$y", "x", "y"], + expected: { path: "$x$y", index: 0, params: { foo: "x", bar: "y" } }, + }, + ], + }, + { + path: "{$:foo}+", + tests: [ + { + input: "$x", + matches: ["$x", "x"], + expected: { path: "$x", index: 0, params: { foo: ["x"] } }, + }, + { + input: "$x$y", + matches: ["$x$y", "x$y"], + expected: { path: "$x$y", index: 0, params: { foo: ["x", "y"] } }, + }, + ], + }, + { + path: "name{/:attr1}?{-:attr2}?{-:attr3}?", + tests: [ + { + input: "name", + matches: ["name", undefined, undefined, undefined], + expected: { path: "name", index: 0, params: {} }, + }, + { + input: "name/test", + matches: ["name/test", "test", undefined, undefined], + expected: { + path: "name/test", + index: 0, + params: { attr1: "test" }, + }, + }, + { + input: "name/1", + matches: ["name/1", "1", undefined, undefined], + expected: { + path: "name/1", + index: 0, + params: { attr1: "1" }, + }, + }, + { + input: "name/1-2", + matches: ["name/1-2", "1", "2", undefined], + expected: { + path: "name/1-2", + index: 0, + params: { attr1: "1", attr2: "2" }, + }, + }, + { + input: "name/1-2-3", + 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, + expected: false, + }, + { + input: "name/test/route", + matches: null, + expected: false, + }, + ], + }, + { + path: "name{/:attrs;-}*", + tests: [ + { + input: "name", + matches: ["name", undefined], + expected: { path: "name", index: 0, params: {} }, + }, + { + input: "name/1", + matches: ["name/1", "1"], + expected: { + path: "name/1", + index: 0, + params: { attrs: ["1"] }, + }, + }, + { + input: "name/1-2", + matches: ["name/1-2", "1-2"], + expected: { + path: "name/1-2", + index: 0, + params: { attrs: ["1", "2"] }, + }, + }, + { + input: "name/1-2-3", + 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, + expected: false, + }, + { + input: "name/test/route", + matches: null, + expected: false, + }, + ], + }, + + /** + * Nested parentheses. + */ + { + path: "/:test(\\d+(?:\\.\\d+)?)", + tests: [ + { + input: "/123", + matches: ["/123", "123"], + expected: { path: "/123", index: 0, params: { test: "123" } }, + }, + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/123/abc", + matches: null, + expected: false, + }, + { + input: "/123.123", + matches: ["/123.123", "123.123"], + expected: { path: "/123.123", index: 0, params: { test: "123.123" } }, + }, + { + input: "/123.abc", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:test((?!login)[^/]+)", + tests: [ + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { test: "route" } }, + }, + { + input: "/login", + matches: null, + expected: false, + }, + ], + }, + + /** + * https://github.com/pillarjs/path-to-regexp/issues/206 + */ + { + path: "/user{(s)}?/:user", + tests: [ + { + input: "/user/123", + matches: ["/user/123", undefined, "123"], + expected: { path: "/user/123", index: 0, params: { user: "123" } }, + }, + { + input: "/users/123", + matches: ["/users/123", "s", "123"], + expected: { + path: "/users/123", + index: 0, + params: { 0: "s", user: "123" }, + }, + }, + ], + }, + { + path: "/user{s}?/:user", + tests: [ + { + input: "/user/123", + matches: ["/user/123", "123"], + expected: { path: "/user/123", index: 0, params: { user: "123" } }, + }, + { + input: "/users/123", + matches: ["/users/123", "123"], + expected: { path: "/users/123", index: 0, params: { user: "123" } }, + }, + ], + }, + + /** + * https://github.com/pillarjs/path-to-regexp/pull/270 + */ + { + path: "/files{/:path}*{.:ext}*", + tests: [ + { + input: "/files/hello/world.txt", + matches: ["/files/hello/world.txt", "hello/world", "txt"], + expected: { + path: "/files/hello/world.txt", + index: 0, + params: { path: ["hello", "world"], ext: ["txt"] }, + }, + }, + { + input: "/files/hello/world.txt.png", + 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 }, + }, + }, + ], + }, + { + path: "/files{/:path}*{.:ext}?", + tests: [ + { + input: "/files/hello/world.txt", + 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 }, + }, + }, + ], + }, + { + path: "#/*", + tests: [ + { + input: "#/", + matches: ["#/", undefined], + expected: { path: "#/", index: 0, params: {} }, + }, + ], + }, + { + path: "/foo{/:bar}*", + tests: [ + { + input: "/foo/test1//test2", + matches: ["/foo/test1//test2", "test1//test2"], + expected: { + path: "/foo/test1//test2", + index: 0, + params: { bar: ["test1", "test2"] }, + }, + }, + ], + }, + { + path: "/entity/:id/*", + tests: [ + { + input: "/entity/foo", + matches: null, + expected: false, + }, + { + input: "/entity/foo/", + matches: ["/entity/foo/", "foo", undefined], + expected: { path: "/entity/foo/", index: 0, params: { id: "foo" } }, + }, + ], + }, + { + path: "/test/*", + tests: [ + { + input: "/test", + matches: null, + expected: false, + }, + { + input: "/test/", + matches: ["/test/", undefined], + expected: { path: "/test/", index: 0, params: {} }, + }, + { + input: "/test/route", + matches: ["/test/route", "route"], + expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/test/route/nested", + matches: ["/test/route/nested", "route/nested"], + expected: { + path: "/test/route/nested", + index: 0, + params: { "0": ["route", "nested"] }, + }, + }, + ], + }, + + /** + * Asterisk wildcard. + */ + { + path: "/*", + tests: [ + { + input: "/", + matches: ["/", undefined], + expected: { path: "/", index: 0, params: { "0": undefined } }, + }, + { + input: "/route", + matches: ["/route", "route"], + expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + }, + { + input: "/route/nested", + matches: ["/route/nested", "route/nested"], + expected: { + path: "/route/nested", + index: 0, + params: { "0": ["route", "nested"] }, + }, + }, + ], + }, + { + path: "*", + tests: [ + { + input: "/", + matches: ["/", "/"], + expected: { path: "/", index: 0, params: { "0": ["", ""] } }, + }, + { + input: "/test", + matches: ["/test", "/test"], + expected: { path: "/test", index: 0, params: { "0": ["", "test"] } }, + }, + ], + }, + { + path: "*", + options: { decode: false }, + tests: [ + { + input: "/", + matches: ["/", "/"], + expected: { path: "/", index: 0, 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, + }, + ], + }, + + /** + * Longer prefix. + */ + { + path: "/:foo{/test/:bar}?", + tests: [ + { + input: "/route", + matches: ["/route", "route", undefined], + expected: { path: "/route", index: 0, 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" }, + }, + }, + ], + }, + + /** + * Prefix and suffix as separator. + */ + { + path: "/{<:foo>}+", + tests: [ + { + input: "/", + matches: ["/", "test"], + expected: { path: "/", index: 0, params: { foo: ["test"] } }, + }, + { + input: "/", + matches: ["/", "test>", + index: 0, + params: { foo: ["test", "again"] }, + }, + }, + ], + }, + + /** + * Backtracking tests. + */ + { + path: "{:foo/}?{:bar.}?", + tests: [ + { + input: "", + matches: ["", undefined, undefined], + expected: { path: "", index: 0, 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" } }, + }, + ], + }, + { + path: "/abc{abc:foo}?", + tests: [ + { + input: "/abc", + matches: ["/abc", undefined], + expected: { path: "/abc", index: 0, params: {} }, + }, + { + input: "/abcabc", + matches: null, + expected: false, + }, + { + input: "/abcabc123", + matches: ["/abcabc123", "123"], + expected: { path: "/abcabc123", index: 0, 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" } }, + }, + ], + }, + { + path: "/:foo{abc:bar}?", + tests: [ + { + input: "/abc", + matches: ["/abc", "abc", undefined], + expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + }, + { + input: "/abcabc", + matches: ["/abcabc", "abcabc", undefined], + expected: { path: "/abcabc", index: 0, params: { foo: "abcabc" } }, + }, + { + 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" }, + }, + }, + ], + }, + { + path: "/:foo\\abc:bar", + tests: [ + { + input: "/abc", + matches: null, + expected: false, + }, + { + input: "/abcabc", + matches: null, + expected: false, + }, + { + 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" }, + }, + }, + ], + }, + { + path: "/:foo(.*){.:ext}?", + tests: [ + { + input: "/abc", + matches: ["/abc", "abc", undefined], + expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + }, + { + input: "/abc.txt", + matches: ["/abc.txt", "abc.txt", undefined], + expected: { path: "/abc.txt", index: 0, params: { foo: "abc.txt" } }, + }, + ], + }, + { + path: "/route|:param|", + tests: [ + { + input: "/route|world|", + matches: ["/route|world|", "world"], + expected: { + path: "/route|world|", + index: 0, + params: { param: "world" }, + }, + }, + { + input: "/route||", + matches: null, + expected: false, + }, + ], + }, + { + path: "/:foo|:bar|", + tests: [ + { + input: "/hello|world|", + matches: ["/hello|world|", "hello", "world"], + expected: { + path: "/hello|world|", + index: 0, + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello||", + matches: null, + expected: false, + }, + ], + }, + { + path: ":foo\\@:bar", + tests: [ + { + input: "x@y", + matches: ["x@y", "x", "y"], + expected: { path: "x@y", index: 0, params: { foo: "x", bar: "y" } }, + }, + { + input: "x@", + matches: null, + expected: false, + }, + ], + }, + + /** + * Multi character delimiters. + */ + { + path: "%25:foo{%25:bar}?", + options: { + delimiter: "%25", + }, + tests: [ + { + input: "%25hello", + matches: ["%25hello", "hello", undefined], + expected: { path: "%25hello", index: 0, params: { foo: "hello" } }, + }, + { + input: "%25hello%25world", + matches: ["%25hello%25world", "hello", "world"], + expected: { + path: "%25hello%25world", + index: 0, + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "%25555%25222", + 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 3f26a97..ffd092f 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,2894 +1,6 @@ -import { describe, it, expect, TestOptions } from "vitest"; -import * as pathToRegexp from "./index"; - -interface ParserTestSet { - path: string; - options?: pathToRegexp.ParseOptions; - expected: pathToRegexp.Token[]; - testOptions?: TestOptions; -} - -interface CompileTestSet { - path: string; - options?: pathToRegexp.CompileOptions; - testOptions?: TestOptions; - tests: Array<{ - input: pathToRegexp.ParamData | undefined; - expected: string | null; - }>; -} - -interface MatchTestSet { - path: pathToRegexp.Path; - options?: pathToRegexp.MatchOptions; - testOptions?: TestOptions; - tests: Array<{ - input: string; - matches: (string | undefined)[] | null; - expected: pathToRegexp.Match; - }>; -} - -const PARSER_TESTS: ParserTestSet[] = [ - { - path: "/", - expected: ["/"], - }, - { - path: "/:test", - expected: ["/", { name: "test" }], - }, - { - path: "/:0", - expected: ["/", { name: "0" }], - }, - { - path: "/:_", - expected: ["/", { name: "_" }], - }, - { - path: "/:café", - expected: ["/", { name: "café" }], - }, -]; - -const COMPILE_TESTS: CompileTestSet[] = [ - { - path: "/", - tests: [ - { input: undefined, expected: "/" }, - { input: {}, expected: "/" }, - { input: { id: "123" }, expected: "/" }, - ], - }, - { - path: "/test", - tests: [ - { input: undefined, expected: "/test" }, - { input: {}, expected: "/test" }, - { input: { id: "123" }, expected: "/test" }, - ], - }, - { - path: "/test/", - tests: [ - { input: undefined, expected: "/test/" }, - { input: {}, expected: "/test/" }, - { input: { id: "123" }, expected: "/test/" }, - ], - }, - { - path: "/:0", - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { 0: "123" }, expected: "/123" }, - ], - }, - { - path: "/:test", - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, - ], - }, - { - path: "/:test", - options: { validate: false }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, - ], - }, - { - path: "/:test", - options: { validate: false, encode: false }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123/xyz" }, - ], - }, - { - path: "/:test", - options: { encode: encodeURIComponent }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, - ], - }, - { - path: "/:test", - options: { encode: () => "static" }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/static" }, - { input: { test: "123/xyz" }, expected: "/static" }, - ], - }, - { - path: "{/:test}?", - options: { encode: false }, - tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: undefined }, expected: "" }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, - ], - }, - { - path: "/:test(.*)", - options: { encode: false }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "" }, expected: "/" }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123/xyz" }, - ], - }, - { - path: "{/:test}*", - tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: [] }, expected: "" }, - { input: { test: [""] }, expected: null }, - { input: { test: ["123"] }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, - { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, - ], - }, - { - path: "{/:test}*", - options: { encode: false }, - tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: "" }, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123/xyz" }, - { input: { test: ["123", "xyz"] }, expected: null }, - ], - }, -]; - -/** - * An array of test cases with expected inputs and outputs. - */ -const MATCH_TESTS: MatchTestSet[] = [ - /** - * Simple paths. - */ - { - path: "/", - tests: [ - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { input: "/route", matches: null, expected: false }, - ], - }, - { - path: "/test", - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { input: "/route", matches: null, expected: false }, - { input: "/test/route", matches: null, expected: false }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - ], - }, - { - path: "/test/", - tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { input: "/route", matches: null, expected: false }, - { input: "/test", matches: null, expected: false }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - ], - }, - { - path: "/:test", - 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: null, - 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é" }, - }, - }, - { - input: "/;,:@&=+$-_.!~*()", - matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], - expected: { - path: "/;,:@&=+$-_.!~*()", - index: 0, - params: { test: ";,:@&=+$-_.!~*()" }, - }, - }, - ], - }, - - /** - * 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: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - ], - }, - { - 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, - }, - { - input: "/route/test//", - matches: null, - expected: false, - }, - { - input: "/route//test", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - ], - }, - - /** - * Non-starting and non-ending modes. - */ - { - path: "/test", - options: { - start: false, - 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/route", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/route/test", - matches: ["/test"], - expected: { path: "/test", index: 6, params: {} }, - }, - ], - }, - { - 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//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - { - input: "/route/test//deep", - matches: ["/test/"], - expected: { path: "/test/", index: 6, params: {} }, - }, - ], - }, - { - path: "/:test", - options: { - start: false, - 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/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: { - start: false, - 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: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, - }, - { - input: "/route/test//", - matches: ["/test//", "test"], - expected: { path: "/test//", index: 6, params: { test: "test" } }, - }, - ], - }, - - /** - * Optional. - */ - { - path: "{/:test}?", - 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///", - matches: ["///route///", "route"], - expected: { path: "///route///", index: 0, params: { test: "route" } }, - }, - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "///", - matches: ["///", undefined], - expected: { path: "///", 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: "/", matches: null, expected: false }, - { input: "///", matches: null, expected: false }, - ], - }, - { - path: "{/:test}?/bar", - tests: [ - { - input: "/bar", - matches: ["/bar", undefined], - expected: { path: "/bar", index: 0, 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" } }, - }, - { - input: "/foo/bar/", - matches: ["/foo/bar/", "foo"], - expected: { path: "/foo/bar/", index: 0, params: { test: "foo" } }, - }, - ], - }, - { - path: "{/:test}?-bar", - tests: [ - { - input: "-bar", - matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, 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" } }, - }, - ], - }, - { - path: "/{:test}?-bar", - tests: [ - { - input: "/-bar", - matches: ["/-bar", undefined], - expected: { path: "/-bar", index: 0, 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" } }, - }, - ], - }, - - /** - * Zero or more times. - */ - { - path: "{/:test}*", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "//", - matches: ["//", undefined], - expected: { path: "//", index: 0, params: {} }, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, 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"] }, - }, - }, - ], - }, - { - path: "{/:test}*-bar", - tests: [ - { - input: "-bar", - matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, params: {} }, - }, - { - input: "/-bar", - matches: null, - expected: false, - }, - { - input: "/foo-bar", - matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, - }, - { - input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], - expected: { - path: "/foo/baz-bar", - index: 0, - params: { test: ["foo", "baz"] }, - }, - }, - ], - }, - - /** - * One or more times. - */ - { - path: "{/:test}+", - tests: [ - { - input: "/", - matches: null, - expected: false, - }, - { - input: "//", - matches: null, - expected: false, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, 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"] }, - }, - }, - ], - }, - { - path: "{/:test}+-bar", - tests: [ - { - input: "-bar", - matches: null, - expected: false, - }, - { - input: "/-bar", - matches: null, - expected: false, - }, - { - input: "/foo-bar", - matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, - }, - { - input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], - expected: { - path: "/foo/baz-bar", - index: 0, - params: { test: ["foo", "baz"] }, - }, - }, - ], - }, - - /** - * Custom parameters. - */ - { - path: String.raw`/:test(\d+)`, - tests: [ - { - input: "/123", - matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, - }, - { - input: "/abc", - matches: null, - expected: false, - }, - { - input: "/123/abc", - matches: null, - expected: false, - }, - ], - }, - { - path: String.raw`/:test(\d+)-bar`, - tests: [ - { - input: "-bar", - matches: null, - expected: false, - }, - { - input: "/-bar", - matches: null, - expected: false, - }, - { - input: "/abc-bar", - matches: null, - expected: false, - }, - { - input: "/123-bar", - matches: ["/123-bar", "123"], - expected: { path: "/123-bar", index: 0, params: { test: "123" } }, - }, - { - input: "/123/456-bar", - matches: null, - expected: false, - }, - ], - }, - { - path: String.raw`/:test(.*)`, - tests: [ - { - input: "/", - matches: ["/", ""], - expected: { path: "/", index: 0, params: { test: "" } }, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/123", - matches: ["/route/123", "route/123"], - expected: { - path: "/route/123", - index: 0, - params: { test: "route/123" }, - }, - }, - { - input: "/;,:@&=/+$-_.!/~*()", - matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], - expected: { - path: "/;,:@&=/+$-_.!/~*()", - index: 0, - params: { test: ";,:@&=/+$-_.!/~*()" }, - }, - }, - ], - }, - { - path: "/:test([a-z]+)", - tests: [ - { - input: "/abc", - matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: "abc" } }, - }, - { - input: "/123", - matches: null, - expected: false, - }, - { - input: "/abc/123", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test(this|that)", - tests: [ - { - input: "/this", - matches: ["/this", "this"], - expected: { path: "/this", index: 0, params: { test: "this" } }, - }, - { - input: "/that", - matches: ["/that", "that"], - expected: { path: "/that", index: 0, params: { test: "that" } }, - }, - { - input: "/foo", - matches: null, - expected: false, - }, - ], - }, - { - path: "{/:test(abc|xyz)}*", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { test: undefined } }, - }, - { - input: "/abc", - matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: ["abc"] } }, - }, - { - input: "/abc/abc", - matches: ["/abc/abc", "abc/abc"], - expected: { - path: "/abc/abc", - index: 0, - params: { test: ["abc", "abc"] }, - }, - }, - { - input: "/xyz/xyz", - matches: ["/xyz/xyz", "xyz/xyz"], - expected: { - path: "/xyz/xyz", - index: 0, - params: { test: ["xyz", "xyz"] }, - }, - }, - { - input: "/abc/xyz", - matches: ["/abc/xyz", "abc/xyz"], - expected: { - path: "/abc/xyz", - index: 0, - params: { test: ["abc", "xyz"] }, - }, - }, - { - input: "/abc/xyz/abc/xyz", - 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, - expected: false, - }, - ], - }, - - /** - * No prefix characters. - */ - { - path: "test", - tests: [ - { - input: "test", - matches: ["test"], - expected: { path: "test", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - ], - }, - { - path: ":test", - tests: [ - { - input: "route", - matches: ["route", "route"], - expected: { path: "route", index: 0, params: { test: "route" } }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "route/", - matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: "route" } }, - }, - ], - }, - { - path: "{:test}?", - tests: [ - { - input: "test", - matches: ["test", "test"], - expected: { path: "test", index: 0, params: { test: "test" } }, - }, - { - input: "", - matches: ["", undefined], - expected: { path: "", index: 0, params: {} }, - }, - ], - }, - { - path: "{:test}*", - testOptions: { - skip: true, - }, - tests: [ - { - input: "test", - matches: ["test", "test"], - expected: { path: "test", index: 0, params: { test: ["test"] } }, - }, - { - input: "test/test", - matches: ["test/test", "test/test"], - expected: { - path: "test/test", - index: 0, - params: { test: ["test", "test"] }, - }, - }, - { - input: "", - matches: ["", undefined], - expected: { path: "", index: 0, params: { test: undefined } }, - }, - ], - }, - { - path: "{:test}+", - testOptions: { - skip: true, - }, - tests: [ - { - input: "test", - matches: ["test", "test"], - expected: { path: "test", index: 0, params: { test: ["test"] } }, - }, - { - input: "test/test", - matches: ["test/test", "test/test"], - expected: { - path: "test/test", - index: 0, - params: { test: ["test", "test"] }, - }, - }, - { - input: "", - matches: null, - expected: false, - }, - ], - }, - { - path: "{:test/}+", - tests: [ - { - input: "route/", - matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: ["route"] } }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "", - matches: null, - expected: false, - }, - { - input: "foo/bar/", - matches: ["foo/bar/", "foo/bar"], - expected: { - path: "foo/bar/", - index: 0, - params: { test: ["foo", "bar"] }, - }, - }, - ], - }, - - /** - * Formats. - */ - { - path: "/test.json", - tests: [ - { - input: "/test.json", - matches: ["/test.json"], - expected: { path: "/test.json", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test.json", - tests: [ - { - input: "/.json", - matches: null, - expected: false, - }, - { - input: "/test.json", - matches: ["/test.json", "test"], - expected: { path: "/test.json", index: 0, params: { test: "test" } }, - }, - { - input: "/route.json", - matches: ["/route.json", "route"], - expected: { path: "/route.json", index: 0, params: { test: "route" } }, - }, - { - input: "/route.json.json", - matches: ["/route.json.json", "route.json"], - expected: { - path: "/route.json.json", - index: 0, - params: { test: "route.json" }, - }, - }, - ], - }, - - /** - * Format params. - */ - { - path: "/test.:format(\\w+)", - tests: [ - { - input: "/test.html", - matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test.:format(\\w+).:format(\\w+)", - tests: [ - { - input: "/test.html.json", - matches: ["/test.html.json", "html", "json"], - expected: { - path: "/test.html.json", - index: 0, - params: { format: "json" }, - }, - }, - { - input: "/test.html", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test{.:format(\\w+)}?", - tests: [ - { - input: "/test", - matches: ["/test", undefined], - expected: { path: "/test", index: 0, params: { format: undefined } }, - }, - { - input: "/test.html", - matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, - }, - ], - }, - { - path: "/test{.:format(\\w+)}+", - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test.html", - matches: ["/test.html", "html"], - expected: { - path: "/test.html", - index: 0, - params: { format: ["html"] }, - }, - }, - { - input: "/test.html.json", - matches: ["/test.html.json", "html.json"], - expected: { - path: "/test.html.json", - index: 0, - params: { format: ["html", "json"] }, - }, - }, - ], - }, - { - path: "/test{.:format}+", - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test.html", - matches: ["/test.html", "html"], - expected: { - path: "/test.html", - index: 0, - params: { format: ["html"] }, - }, - }, - { - input: "/test.hbs.html", - matches: ["/test.hbs.html", "hbs.html"], - expected: { - path: "/test.hbs.html", - index: 0, - params: { format: ["hbs", "html"] }, - }, - }, - ], - }, - - /** - * Format and path params. - */ - { - path: "/:test.:format", - tests: [ - { - input: "/route.html", - matches: ["/route.html", "route", "html"], - expected: { - path: "/route.html", - index: 0, - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route.html.json", - matches: ["/route.html.json", "route", "html.json"], - expected: { - path: "/route.html.json", - index: 0, - params: { test: "route", format: "html.json" }, - }, - }, - ], - }, - { - path: "/:test{.:format}?", - tests: [ - { - input: "/route", - matches: ["/route", "route", undefined], - 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", format: "json" }, - }, - }, - { - input: "/route.json.html", - matches: ["/route.json.html", "route", "json.html"], - expected: { - path: "/route.json.html", - index: 0, - params: { test: "route", format: "json.html" }, - }, - }, - ], - }, - { - path: "/:test.:format\\z", - tests: [ - { - input: "/route.htmlz", - matches: ["/route.htmlz", "route", "html"], - expected: { - path: "/route.htmlz", - index: 0, - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route.html", - matches: null, - expected: false, - }, - ], - }, - - /** - * Unnamed params. - */ - { - path: "/(\\d+)", - tests: [ - { - input: "/123", - matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, - }, - { - input: "/abc", - matches: null, - expected: false, - }, - { - input: "/123/abc", - matches: null, - expected: false, - }, - ], - }, - { - path: "{/(\\d+)}?", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, - }, - { - input: "/123", - matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, - }, - ], - }, - { - path: "/route\\(\\\\(\\d+\\\\)\\)", - tests: [ - { - input: "/route(\\123\\)", - matches: ["/route(\\123\\)", "123\\"], - expected: { - path: "/route(\\123\\)", - index: 0, - params: { "0": "123\\" }, - }, - }, - { - input: "/route(\\123)", - matches: null, - expected: false, - }, - ], - }, - { - path: "{/route}?", - tests: [ - { - input: "", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "/foo", - matches: null, - expected: false, - }, - { - input: "/route", - matches: ["/route"], - expected: { path: "/route", index: 0, params: {} }, - }, - ], - }, - { - path: "{/(.*)}", - tests: [ - { - input: "/", - matches: ["/", ""], - expected: { path: "/", index: 0, params: { "0": "" } }, - }, - { - input: "/login", - matches: ["/login", "login"], - expected: { path: "/login", index: 0, params: { "0": "login" } }, - }, - ], - }, - - /** - * Escaped characters. - */ - { - path: "/\\(testing\\)", - tests: [ - { - input: "/testing", - matches: null, - expected: false, - }, - { - input: "/(testing)", - matches: ["/(testing)"], - expected: { path: "/(testing)", index: 0, params: {} }, - }, - ], - }, - { - path: "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", - tests: [ - { - input: "/.+*?{}=^!:$[]|", - matches: ["/.+*?{}=^!:$[]|"], - expected: { path: "/.+*?{}=^!:$[]|", index: 0, params: {} }, - }, - ], - }, - { - path: "/test/{:uid(u\\d+)}?{:cid(c\\d+)}?", - tests: [ - { - input: "/test/u123", - matches: ["/test/u123", "u123", undefined], - expected: { path: "/test/u123", index: 0, params: { uid: "u123" } }, - }, - { - input: "/test/c123", - matches: ["/test/c123", undefined, "c123"], - expected: { path: "/test/c123", index: 0, params: { cid: "c123" } }, - }, - ], - }, - - /** - * Unnamed group prefix. - */ - { - path: "/{apple-}?icon-:res(\\d+).png", - tests: [ - { - input: "/icon-240.png", - matches: ["/icon-240.png", "240"], - expected: { path: "/icon-240.png", index: 0, 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" }, - }, - }, - ], - }, - - /** - * Random examples. - */ - { - path: "/:foo/:bar", - tests: [ - { - input: "/match/route", - matches: ["/match/route", "match", "route"], - expected: { - path: "/match/route", - index: 0, - params: { foo: "match", bar: "route" }, - }, - }, - ], - }, - { - path: "/:foo\\(test\\)/bar", - tests: [ - { - input: "/foo(test)/bar", - matches: ["/foo(test)/bar", "foo"], - expected: { path: "/foo(test)/bar", index: 0, params: { foo: "foo" } }, - }, - { - input: "/foo/bar", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:remote([\\w-.]+)/:user([\\w-]+)", - tests: [ - { - input: "/endpoint/user", - matches: ["/endpoint/user", "endpoint", "user"], - expected: { - path: "/endpoint/user", - index: 0, - params: { remote: "endpoint", user: "user" }, - }, - }, - { - input: "/endpoint/user-name", - matches: ["/endpoint/user-name", "endpoint", "user-name"], - expected: { - path: "/endpoint/user-name", - index: 0, - params: { remote: "endpoint", user: "user-name" }, - }, - }, - { - input: "/foo.bar/user-name", - matches: ["/foo.bar/user-name", "foo.bar", "user-name"], - expected: { - path: "/foo.bar/user-name", - index: 0, - params: { remote: "foo.bar", user: "user-name" }, - }, - }, - ], - }, - { - path: "/:foo\\?", - tests: [ - { - input: "/route?", - matches: ["/route?", "route"], - expected: { path: "/route?", index: 0, params: { foo: "route" } }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "{/:foo}+bar", - tests: [ - { - input: "/foobar", - matches: ["/foobar", "foo"], - expected: { path: "/foobar", index: 0, params: { foo: ["foo"] } }, - }, - { - input: "/foo/bar", - matches: null, - expected: false, - }, - { - input: "/foo/barbar", - matches: ["/foo/barbar", "foo/bar"], - expected: { - path: "/foo/barbar", - index: 0, - params: { foo: ["foo", "bar"] }, - }, - }, - ], - }, - { - path: "/{:pre}?baz", - tests: [ - { - input: "/foobaz", - matches: ["/foobaz", "foo"], - expected: { path: "/foobaz", index: 0, params: { pre: "foo" } }, - }, - { - input: "/baz", - matches: ["/baz", undefined], - expected: { path: "/baz", index: 0, params: { pre: undefined } }, - }, - ], - }, - { - path: "/:foo\\({:bar}?\\)", - tests: [ - { - input: "/hello(world)", - matches: ["/hello(world)", "hello", "world"], - expected: { - path: "/hello(world)", - index: 0, - params: { foo: "hello", bar: "world" }, - }, - }, - { - input: "/hello()", - matches: ["/hello()", "hello", undefined], - expected: { - path: "/hello()", - index: 0, - params: { foo: "hello", bar: undefined }, - }, - }, - ], - }, - { - path: "/:postType(video|audio|text){(\\+.+)}?", - tests: [ - { - input: "/video", - matches: ["/video", "video", undefined], - expected: { path: "/video", index: 0, 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, - expected: false, - }, - ], - }, - { - path: "{/:foo}?{/:bar}?-ext", - tests: [ - { - input: "/-ext", - matches: null, - expected: false, - }, - { - input: "-ext", - 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" } }, - }, - { - input: "/foo/bar-ext", - matches: ["/foo/bar-ext", "foo", "bar"], - expected: { - path: "/foo/bar-ext", - index: 0, - params: { foo: "foo", bar: "bar" }, - }, - }, - { - input: "/foo/-ext", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:required{/:optional}?-ext", - tests: [ - { - input: "/foo-ext", - matches: ["/foo-ext", "foo", undefined], - expected: { path: "/foo-ext", index: 0, 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" }, - }, - }, - { - input: "/foo/-ext", - matches: null, - expected: false, - }, - ], - }, - - /** - * Unicode matches. - */ - { - path: "/:foo", - tests: [ - { - input: "/café", - matches: ["/café", "café"], - expected: { path: "/café", index: 0, params: { foo: "café" } }, - }, - ], - }, - { - path: "/:foo", - options: { - decode: false, - }, - tests: [ - { - input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], - expected: { - path: "/caf%C3%A9", - index: 0, - params: { foo: "caf%C3%A9" }, - }, - }, - ], - }, - { - path: "/café", - tests: [ - { - input: "/café", - matches: ["/café"], - expected: { path: "/café", index: 0, params: {} }, - }, - ], - }, - { - path: "/café", - options: { - encodePath: encodeURI, - }, - tests: [ - { - input: "/caf%C3%A9", - matches: ["/caf%C3%A9"], - expected: { path: "/caf%C3%A9", index: 0, params: {} }, - }, - ], - }, - - /** - * Hostnames. - */ - { - path: ":domain.com", - options: { - delimiter: ".", - }, - tests: [ - { - input: "example.com", - matches: ["example.com", "example"], - expected: { - path: "example.com", - index: 0, - params: { domain: "example" }, - }, - }, - { - input: "github.com", - matches: ["github.com", "github"], - expected: { - path: "github.com", - index: 0, - params: { domain: "github" }, - }, - }, - ], - }, - { - path: "mail.:domain.com", - options: { - delimiter: ".", - }, - tests: [ - { - input: "mail.example.com", - matches: ["mail.example.com", "example"], - expected: { - path: "mail.example.com", - index: 0, - params: { domain: "example" }, - }, - }, - { - input: "mail.github.com", - matches: ["mail.github.com", "github"], - expected: { - path: "mail.github.com", - index: 0, - params: { domain: "github" }, - }, - }, - ], - }, - { - path: "mail{.:domain}?.com", - options: { - delimiter: ".", - }, - tests: [ - { - input: "mail.com", - matches: ["mail.com", undefined], - expected: { path: "mail.com", index: 0, params: { domain: undefined } }, - }, - { - input: "mail.example.com", - matches: ["mail.example.com", "example"], - expected: { - path: "mail.example.com", - index: 0, - params: { domain: "example" }, - }, - }, - { - input: "mail.github.com", - matches: ["mail.github.com", "github"], - expected: { - path: "mail.github.com", - index: 0, - params: { domain: "github" }, - }, - }, - ], - }, - { - path: "example.:ext", - options: { - delimiter: ".", - }, - tests: [ - { - input: "example.com", - matches: ["example.com", "com"], - expected: { path: "example.com", index: 0, params: { ext: "com" } }, - }, - { - input: "example.org", - matches: ["example.org", "org"], - expected: { path: "example.org", index: 0, params: { ext: "org" } }, - }, - ], - }, - { - path: "this is", - options: { - delimiter: " ", - end: false, - }, - tests: [ - { - input: "this is a test", - matches: ["this is"], - expected: { path: "this is", index: 0, params: {} }, - }, - { - input: "this isn't", - matches: null, - expected: false, - }, - ], - }, - - /** - * Prefixes. - */ - { - path: "{$:foo}{$:bar}?", - tests: [ - { - input: "$x", - matches: ["$x", "x", undefined], - expected: { path: "$x", index: 0, params: { foo: "x" } }, - }, - { - input: "$x$y", - matches: ["$x$y", "x", "y"], - expected: { path: "$x$y", index: 0, params: { foo: "x", bar: "y" } }, - }, - ], - }, - { - path: "{$:foo}+", - tests: [ - { - input: "$x", - matches: ["$x", "x"], - expected: { path: "$x", index: 0, params: { foo: ["x"] } }, - }, - { - input: "$x$y", - matches: ["$x$y", "x$y"], - expected: { path: "$x$y", index: 0, params: { foo: ["x", "y"] } }, - }, - ], - }, - { - path: "name{/:attr1}?{-:attr2}?{-:attr3}?", - tests: [ - { - input: "name", - matches: ["name", undefined, undefined, undefined], - expected: { path: "name", index: 0, params: {} }, - }, - { - input: "name/test", - matches: ["name/test", "test", undefined, undefined], - expected: { - path: "name/test", - index: 0, - params: { attr1: "test" }, - }, - }, - { - input: "name/1", - matches: ["name/1", "1", undefined, undefined], - expected: { - path: "name/1", - index: 0, - params: { attr1: "1" }, - }, - }, - { - input: "name/1-2", - matches: ["name/1-2", "1", "2", undefined], - expected: { - path: "name/1-2", - index: 0, - params: { attr1: "1", attr2: "2" }, - }, - }, - { - input: "name/1-2-3", - 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, - expected: false, - }, - { - input: "name/test/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "name{/:attrs;-}*", - tests: [ - { - input: "name", - matches: ["name", undefined], - expected: { path: "name", index: 0, params: {} }, - }, - { - input: "name/1", - matches: ["name/1", "1"], - expected: { - path: "name/1", - index: 0, - params: { attrs: ["1"] }, - }, - }, - { - input: "name/1-2", - matches: ["name/1-2", "1-2"], - expected: { - path: "name/1-2", - index: 0, - params: { attrs: ["1", "2"] }, - }, - }, - { - input: "name/1-2-3", - 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, - expected: false, - }, - { - input: "name/test/route", - matches: null, - expected: false, - }, - ], - }, - - /** - * Nested parentheses. - */ - { - path: "/:test(\\d+(?:\\.\\d+)?)", - tests: [ - { - input: "/123", - matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, - }, - { - input: "/abc", - matches: null, - expected: false, - }, - { - input: "/123/abc", - matches: null, - expected: false, - }, - { - input: "/123.123", - matches: ["/123.123", "123.123"], - expected: { path: "/123.123", index: 0, params: { test: "123.123" } }, - }, - { - input: "/123.abc", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test((?!login)[^/]+)", - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/login", - matches: null, - expected: false, - }, - ], - }, - - /** - * https://github.com/pillarjs/path-to-regexp/issues/206 - */ - { - path: "/user{(s)}?/:user", - tests: [ - { - input: "/user/123", - matches: ["/user/123", undefined, "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, - }, - { - input: "/users/123", - matches: ["/users/123", "s", "123"], - expected: { - path: "/users/123", - index: 0, - params: { 0: "s", user: "123" }, - }, - }, - ], - }, - { - path: "/user{s}?/:user", - tests: [ - { - input: "/user/123", - matches: ["/user/123", "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, - }, - { - input: "/users/123", - matches: ["/users/123", "123"], - expected: { path: "/users/123", index: 0, params: { user: "123" } }, - }, - ], - }, - - /** - * https://github.com/pillarjs/path-to-regexp/pull/270 - */ - { - path: "/files{/:path}*{.:ext}*", - tests: [ - { - input: "/files/hello/world.txt", - 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 }, - }, - }, - ], - }, - { - path: "#/*", - tests: [ - { - input: "#/", - matches: ["#/", undefined], - expected: { path: "#/", index: 0, params: {} }, - }, - ], - }, - { - path: "/foo{/:bar}*", - tests: [ - { - input: "/foo/test1//test2", - matches: ["/foo/test1//test2", "test1//test2"], - expected: { - path: "/foo/test1//test2", - index: 0, - params: { bar: ["test1", "test2"] }, - }, - }, - ], - }, - { - path: "/entity/:id/*", - tests: [ - { - input: "/entity/foo", - matches: null, - expected: false, - }, - { - input: "/entity/foo/", - matches: ["/entity/foo/", "foo", undefined], - expected: { path: "/entity/foo/", index: 0, params: { id: "foo" } }, - }, - ], - }, - { - path: "/test/*", - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/", - matches: ["/test/", undefined], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: ["/test/route", "route"], - expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, - }, - { - input: "/test/route/nested", - matches: ["/test/route/nested", "route/nested"], - expected: { - path: "/test/route/nested", - index: 0, - params: { "0": ["route", "nested"] }, - }, - }, - ], - }, - - /** - * Asterisk wildcard. - */ - { - path: "/*", - tests: [ - { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, - }, - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": ["route"] } }, - }, - { - input: "/route/nested", - matches: ["/route/nested", "route/nested"], - expected: { - path: "/route/nested", - index: 0, - params: { "0": ["route", "nested"] }, - }, - }, - ], - }, - { - path: "*", - tests: [ - { - input: "/", - matches: ["/", "/"], - expected: { path: "/", index: 0, params: { "0": ["", ""] } }, - }, - { - input: "/test", - matches: ["/test", "/test"], - expected: { path: "/test", index: 0, params: { "0": ["", "test"] } }, - }, - ], - }, - { - path: "*", - options: { decode: false }, - tests: [ - { - input: "/", - matches: ["/", "/"], - expected: { path: "/", index: 0, 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, - }, - ], - }, -]; +import { describe, it, expect } from "vitest"; +import { pathToRegexp, parse, compile, match } from "./index.js"; +import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; /** * Dynamically generate the entire test suite. @@ -2896,7 +8,7 @@ const MATCH_TESTS: MatchTestSet[] = [ describe("path-to-regexp", () => { describe("arguments", () => { it("should accept an array of keys as the second argument", () => { - const re = pathToRegexp.pathToRegexp("/user/:id", { end: false }); + const re = pathToRegexp("/user/:id", { end: false }); const expectedKeys = [ { @@ -2910,57 +22,67 @@ describe("path-to-regexp", () => { }); it("should accept parse result as input", () => { - const tokens = pathToRegexp.parse("/user/:id"); - const re = pathToRegexp.pathToRegexp(tokens); + 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.pathToRegexp("/:foo(?:\\d+(\\.\\d+)?)"); + pathToRegexp("/:foo(?:\\d+(\\.\\d+)?)"); }).toThrow(new TypeError('Pattern cannot start with "?" at 6')); }); it("should throw on nested capturing group", () => { expect(() => { - pathToRegexp.pathToRegexp("/:foo(\\d+(\\.\\d+)?)"); + pathToRegexp("/:foo(\\d+(\\.\\d+)?)"); }).toThrow(new TypeError("Capturing groups are not allowed at 9")); }); it("should throw on unbalanced pattern", () => { expect(() => { - pathToRegexp.pathToRegexp("/:foo(abc"); + pathToRegexp("/:foo(abc"); }).toThrow(new TypeError("Unbalanced pattern at 5")); }); it("should throw on missing pattern", () => { expect(() => { - pathToRegexp.pathToRegexp("/:foo()"); + pathToRegexp("/:foo()"); }).toThrow(new TypeError("Missing pattern at 5")); }); it("should throw on missing name", () => { expect(() => { - pathToRegexp.pathToRegexp("/:(test)"); + pathToRegexp("/:(test)"); }).toThrow(new TypeError("Missing parameter name at 2")); }); it("should throw on nested groups", () => { expect(() => { - pathToRegexp.pathToRegexp("/{a{b:foo}}"); + pathToRegexp("/{a{b:foo}}"); }).toThrow( new TypeError( "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", ), ); }); + + it("should throw on repeat parameters without a separator", () => { + expect(() => { + pathToRegexp("{:x}*"); + }).toThrow( + new TypeError( + `Missing separator for "x": https://git.new/pathToRegexpError`, + ), + ); + }); }); describe.each(PARSER_TESTS)( "parse $path with $options", - ({ path, options, expected, testOptions }) => { - it("should parse the path", testOptions, () => { - const data = pathToRegexp.parse(path, options); + ({ path, options, expected }) => { + it("should parse the path", () => { + const data = parse(path, options); expect(data.tokens).toEqual(expected); }); }, @@ -2968,43 +90,35 @@ describe("path-to-regexp", () => { describe.each(COMPILE_TESTS)( "compile $path with $options", - ({ path, options, tests, testOptions = {} }) => { - it.each(tests)( - "should compile $input", - testOptions, - ({ input, expected }) => { - const toPath = pathToRegexp.compile(path, options); - - if (expected === null) { - expect(() => toPath(input)).toThrow(); - } else { - expect(toPath(input)).toEqual(expected); - } - }, - ); + ({ path, options, tests }) => { + it.each(tests)("should compile $input", ({ input, expected }) => { + const toPath = compile(path, options); + + if (expected === null) { + expect(() => toPath(input)).toThrow(); + } else { + expect(toPath(input)).toEqual(expected); + } + }); }, ); describe.each(MATCH_TESTS)( "match $path with $options", - ({ path, options, tests, testOptions = {} }) => { - it.each(tests)( - "should match $input", - testOptions, - ({ input, matches, expected }) => { - const re = pathToRegexp.pathToRegexp(path, options); - const match = pathToRegexp.match(path, 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(match(input)).toEqual(expected); - }, - ); + expect(exec(re, input)).toEqual(matches); + expect(fn(input)).toEqual(expected); + }); }, ); describe("compile errors", () => { it("should throw when a required param is undefined", () => { - const toPath = pathToRegexp.compile("/a/:b/c"); + const toPath = compile("/a/:b/c"); expect(() => { toPath(); @@ -3012,7 +126,7 @@ describe("path-to-regexp", () => { }); it("should throw when it does not match the pattern", () => { - const toPath = pathToRegexp.compile("/:foo(\\d+)"); + const toPath = compile("/:foo(\\d+)"); expect(() => { toPath({ foo: "abc" }); @@ -3020,7 +134,7 @@ describe("path-to-regexp", () => { }); it("should throw when expecting a repeated value", () => { - const toPath = pathToRegexp.compile("{/:foo}+"); + const toPath = compile("{/:foo}+"); expect(() => { toPath({ foo: [] }); @@ -3028,7 +142,7 @@ describe("path-to-regexp", () => { }); it("should throw when not expecting a repeated value", () => { - const toPath = pathToRegexp.compile("/:foo"); + const toPath = compile("/:foo"); expect(() => { toPath({ foo: [] }); @@ -3036,7 +150,7 @@ describe("path-to-regexp", () => { }); it("should throw when a repeated param is not an array", () => { - const toPath = pathToRegexp.compile("{/:foo}+"); + const toPath = compile("{/:foo}+"); expect(() => { toPath({ foo: "a" }); @@ -3044,7 +158,7 @@ describe("path-to-regexp", () => { }); it("should throw when an array value is not a string", () => { - const toPath = pathToRegexp.compile("{/:foo}+"); + const toPath = compile("{/:foo}+"); expect(() => { toPath({ foo: [1, "a"] }); @@ -3052,7 +166,7 @@ describe("path-to-regexp", () => { }); it("should throw when repeated value does not match", () => { - const toPath = pathToRegexp.compile("{/:foo(\\d+)}+"); + const toPath = compile("{/:foo(\\d+)}+"); expect(() => { toPath({ foo: ["1", "2", "3", "a"] }); diff --git a/src/index.ts b/src/index.ts index e727841..e0165ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ const DEFAULT_DELIMITER = "/"; const NOOP_VALUE = (value: string) => value; const ID_CHAR = /^\p{XID_Continue}$/u; +const DEBUG_URL = "https://git.new/pathToRegexpError"; /** * Encode a string into another string. @@ -14,34 +15,38 @@ export type Decode = (value: string) => string; export interface ParseOptions { /** - * Set the default delimiter for repeat parameters. (default: `'/'`) + * The default delimiter for segments. (default: `'/'`) */ delimiter?: string; /** - * Function for encoding input strings for output into path. + * A function for encoding input strings. */ encodePath?: Encode; } export interface PathToRegexpOptions extends ParseOptions { /** - * When `true` the regexp will be case sensitive. (default: `false`) + * Regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; /** - * Allow delimiter to be arbitrarily repeated. (default: `true`) + * Allow the delimiter to be arbitrarily repeated. (default: `true`) */ loose?: boolean; /** - * When `true` the regexp will match to the end of the string. (default: `true`) + * Verify patterns are valid and safe to use. (default: `false`) */ - end?: boolean; + strict?: boolean; /** - * When `true` the regexp will match from the beginning of the string. (default: `true`) + * Match from the beginning of the string. (default: `true`) */ start?: boolean; /** - * When `true` the regexp allows an optional trailing delimiter to match. (default: `true`) + * Match to the end of the string. (default: `true`) + */ + end?: boolean; + /** + * Allow optional trailing delimiter to match. (default: `true`) */ trailing?: boolean; } @@ -55,15 +60,19 @@ export interface MatchOptions extends PathToRegexpOptions { export interface CompileOptions extends ParseOptions { /** - * When `true` the validation will be case sensitive. (default: `false`) + * Regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; /** - * Allow delimiter to be arbitrarily repeated. (default: `true`) + * Allow the delimiter to be arbitrarily repeated. (default: `true`) */ loose?: boolean; /** - * When `false` the function can produce an invalid (unmatched) path. (default: `true`) + * Verify patterns are valid and safe to use. (default: `false`) + */ + strict?: boolean; + /** + * Verifies the function is producing a valid path. (default: `true`) */ validate?: boolean; /** @@ -75,6 +84,7 @@ export interface CompileOptions extends ParseOptions { type TokenType = | "{" | "}" + | ";" | "*" | "+" | "?" @@ -86,8 +96,7 @@ type TokenType = // Reserved for use. | "!" | "@" - | "," - | ";"; + | ","; /** * Tokenizer results. @@ -214,7 +223,7 @@ class Iter { if (value !== undefined) return value; const { type: nextType, index } = this.peek(); throw new TypeError( - `Unexpected ${nextType} at ${index}, expected ${type}: https://git.new/pathToRegexpError`, + `Unexpected ${nextType} at ${index}, expected ${type}: ${DEBUG_URL}`, ); } @@ -227,10 +236,8 @@ class Iter { return result; } - modifier(): string { - return ( - this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+") || "" - ); + modifier(): string | undefined { + return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); } } @@ -248,7 +255,8 @@ export class TokenData { * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): TokenData { - const { delimiter = DEFAULT_DELIMITER, encodePath = NOOP_VALUE } = options; + const { encodePath = NOOP_VALUE, delimiter = encodePath(DEFAULT_DELIMITER) } = + options; const tokens: Token[] = []; const it = lexer(str); let key = 0; @@ -269,7 +277,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { const next = it.peek(); if (next.type === "*") { throw new TypeError( - `Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: https://git.new/pathToRegexpError`, + `Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: ${DEBUG_URL}`, ); } @@ -280,7 +288,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - pattern: `[^${escape(delimiter)}]*`, + pattern: `(?:(?!${escape(delimiter)}).)*`, modifier: "*", separator: delimiter, }); @@ -293,7 +301,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { const name = it.tryConsume("NAME"); const pattern = it.tryConsume("PATTERN"); const suffix = it.text(); - const separator = it.tryConsume(";") ? it.text() : prefix + suffix; + const separator = it.tryConsume(";") && it.text(); it.consume("}"); @@ -345,7 +353,7 @@ function tokenToFunction( const encodeValue = encode || NOOP_VALUE; const repeated = token.modifier === "+" || token.modifier === "*"; const optional = token.modifier === "?" || token.modifier === "*"; - const { prefix = "", suffix = "", separator = "" } = token; + const { prefix = "", suffix = "", separator = suffix + prefix } = token; if (encode && repeated) { const stringify = (value: string, index: number) => { @@ -411,19 +419,19 @@ function compileTokens

( encode = encodeURIComponent, loose = true, validate = true, + strict = false, } = options; - const reFlags = flags(options); + const flags = toFlags(options); const stringify = toStringify(loose, data.delimiter); - const keyToRegexp = toKeyRegexp(stringify, data.delimiter); + const sources = toRegExpSource(data, stringify, [], flags, strict); // Compile all the tokens into regexps. const encoders: Array<(data: ParamData) => string> = data.tokens.map( - (token) => { + (token, index) => { const fn = tokenToFunction(token, encode); if (!validate || typeof token === "string") return fn; - const pattern = keyToRegexp(token); - const validRe = new RegExp(`^${pattern}$`, reFlags); + const validRe = new RegExp(`^${sources[index]}$`, flags); return (data) => { const value = fn(data); @@ -478,15 +486,16 @@ export function match

( const decoders = keys.map((key) => { if (decode && (key.modifier === "+" || key.modifier === "*")) { - const re = new RegExp(stringify(key.separator || ""), "g"); + const { prefix = "", suffix = "", separator = suffix + prefix } = key; + const re = new RegExp(stringify(separator), "g"); return (value: string) => value.split(re).map(decode); } return decode || NOOP_VALUE; }); - return function match(pathname: string) { - const m = re.exec(pathname); + return function match(input: string) { + const m = re.exec(input); if (!m) return false; const { 0: path, index } = m; @@ -508,14 +517,15 @@ export function match

( * Escape a regular expression string. */ function escape(str: string) { - return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); + return str.replace(/([.+*?^${}()[\]|/\\])/g, "\\$1"); } /** * Escape and repeat loose characters for regular expressions. */ function looseReplacer(value: string, loose: string) { - return loose ? `${escape(value)}+` : escape(value); + const escaped = escape(value); + return loose ? `(?:${escaped})+(?!${escaped})` : escaped; } /** @@ -524,14 +534,14 @@ function looseReplacer(value: string, loose: string) { function toStringify(loose: boolean, delimiter: string) { if (!loose) return escape; - const re = new RegExp(`[^${escape(delimiter)}]+|(.)`, "g"); + const re = new RegExp(`(?:(?!${escape(delimiter)}).)+|(.)`, "g"); return (value: string) => value.replace(re, looseReplacer); } /** * Get the flags for a regexp from the options. */ -function flags(options: { sensitive?: boolean }) { +function toFlags(options: { sensitive?: boolean }) { return options.sensitive ? "" : "i"; } @@ -560,49 +570,107 @@ function tokensToRegexp( keys: Key[], options: PathToRegexpOptions, ): RegExp { - const { trailing = true, start = true, end = true, loose = true } = options; + const { + trailing = true, + loose = true, + start = true, + end = true, + strict = false, + } = options; + const flags = toFlags(options); const stringify = toStringify(loose, data.delimiter); - const keyToRegexp = toKeyRegexp(stringify, data.delimiter); + const sources = toRegExpSource(data, stringify, keys, flags, strict); let pattern = start ? "^" : ""; - - for (const token of data.tokens) { - if (typeof token === "string") { - pattern += stringify(token); - } else { - if (token.name) keys.push(token); - pattern += keyToRegexp(token); - } - } - + pattern += sources.join(""); if (trailing) pattern += `(?:${stringify(data.delimiter)})?`; pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; - return new RegExp(pattern, flags(options)); + return new RegExp(pattern, flags); } /** * Convert a token into a regexp string (re-used for path validation). */ -function toKeyRegexp(stringify: Encode, delimiter: string) { - const segmentPattern = `[^${escape(delimiter)}]+?`; - - return (key: Key) => { - const prefix = key.prefix ? stringify(key.prefix) : ""; - const suffix = key.suffix ? stringify(key.suffix) : ""; - const modifier = key.modifier || ""; - - if (key.name) { - const pattern = key.pattern || segmentPattern; - if (key.modifier === "+" || key.modifier === "*") { - const mod = key.modifier === "*" ? "?" : ""; - const split = key.separator ? stringify(key.separator) : ""; - return `(?:${prefix}((?:${pattern})(?:${split}(?:${pattern}))*)${suffix})${mod}`; +function toRegExpSource( + data: TokenData, + stringify: Encode, + keys: Key[], + flags: string, + strict: boolean, +): string[] { + const defaultPattern = `(?:(?!${escape(data.delimiter)}).)+?`; + let backtrack = ""; + let safe = true; + + return data.tokens.map((token, index) => { + if (typeof token === "string") { + backtrack = token; + return stringify(token); + } + + const { + prefix = "", + suffix = "", + separator = suffix + prefix, + modifier = "", + } = token; + + const pre = stringify(prefix); + const post = stringify(suffix); + + if (token.name) { + const pattern = token.pattern ? `(?:${token.pattern})` : defaultPattern; + const re = checkPattern(pattern, token.name, flags); + + safe ||= safePattern(re, prefix || backtrack); + if (!safe) { + throw new TypeError( + `Ambiguous pattern for "${token.name}": ${DEBUG_URL}`, + ); } - return `(?:${prefix}(${pattern})${suffix})${modifier}`; + safe = !strict || safePattern(re, suffix); + backtrack = ""; + + keys.push(token); + + if (modifier === "+" || modifier === "*") { + const mod = modifier === "*" ? "?" : ""; + const sep = stringify(separator); + + if (!sep) { + throw new TypeError( + `Missing separator for "${token.name}": ${DEBUG_URL}`, + ); + } + + 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}`; + } + + return `(?:${pre}(${pattern})${post})${modifier}`; } - return `(?:${prefix}${suffix})${modifier}`; - }; + 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; } /** @@ -610,8 +678,6 @@ function toKeyRegexp(stringify: Encode, delimiter: string) { */ export type Path = string | TokenData; -export type PathRegExp = RegExp & { keys: Key[] }; - /** * Normalize the given path string, returning a regular expression. * From 7015c1fd7760df38458268d36c441b0782bdf2b4 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Sat, 13 Jul 2024 19:31:49 -0400 Subject: [PATCH 35/55] Better type for compile (#307) --- src/index.spec.ts | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index ffd092f..d0dd420 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -161,7 +161,7 @@ describe("path-to-regexp", () => { const toPath = compile("{/:foo}+"); expect(() => { - toPath({ foo: [1, "a"] }); + toPath({ foo: [1, "a"] as any }); }).toThrow(new TypeError('Expected "foo/0" to be a string')); }); diff --git a/src/index.ts b/src/index.ts index e0165ea..3f0721a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -328,7 +328,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { /** * Compile a string to a template function for the path. */ -export function compile

( +export function compile

( path: Path, options: CompileOptions = {}, ) { From 46b9f0b224ade156637d2d480e5a1bfce2f387b3 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sat, 13 Jul 2024 16:33:50 -0700 Subject: [PATCH 36/55] Add strict option to README --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 66cbc29..dc267b7 100644 --- a/Readme.md +++ b/Readme.md @@ -33,6 +33,7 @@ The `pathToRegexp` function returns a regular expression with `keys` as a proper - **options** _(optional)_ - **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`) From c36bdfa2aa363f573439de9098c2dcc94b16e1e6 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sat, 13 Jul 2024 16:49:30 -0700 Subject: [PATCH 37/55] 7.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ce4895..172848d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "path-to-regexp", - "version": "7.0.0", + "version": "7.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "path-to-regexp", - "version": "7.0.0", + "version": "7.1.0", "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", diff --git a/package.json b/package.json index e99e651..e1c220a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "path-to-regexp", - "version": "7.0.0", + "version": "7.1.0", "description": "Express style path to RegExp utility", "keywords": [ "express", From 0e3b1692993fe9ca86bb2b50462e4981d3e09054 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:24:04 -0700 Subject: [PATCH 38/55] Remove script from code coverage --- vitest.config.mts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 vitest.config.mts diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..f98f607 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,10 @@ +/// +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + coverage: { + exclude: ["scripts/**"], + }, + }, +}); From a9909e45da991c558ab4ae443e2a021ac607db8a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:26:37 -0700 Subject: [PATCH 39/55] Allow parameter names to be quoted --- src/index.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3f0721a..fc5af69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ const DEFAULT_DELIMITER = "/"; const NOOP_VALUE = (value: string) => value; -const ID_CHAR = /^\p{XID_Continue}$/u; +const ID_START = /^[$_\p{ID_Start}]$/u; +const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u; const DEBUG_URL = "https://git.new/pathToRegexpError"; /** @@ -144,12 +145,35 @@ function lexer(str: string) { if (value === ":") { let name = ""; - while (ID_CHAR.test(chars[++i])) { + if (ID_START.test(chars[++i])) { name += chars[i]; + while (ID_CONTINUE.test(chars[++i])) { + name += chars[i]; + } + } else if (chars[i] === '"') { + let pos = i; + + while (i < chars.length) { + if (chars[++i] === '"') { + i++; + pos = 0; + break; + } + + if (chars[i] === "\\") { + name += chars[++i]; + } else { + name += chars[i]; + } + } + + if (pos) { + throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); + } } if (!name) { - throw new TypeError(`Missing parameter name at ${i}`); + throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); } tokens.push({ type: "NAME", index: i, value: name }); From cbf2c73c865e5cf8faeda8564b7917b75c277ae9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:27:16 -0700 Subject: [PATCH 40/55] Add debug URL to all errors --- src/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index fc5af69..2dd3d6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -186,7 +186,9 @@ function lexer(str: string) { let pattern = ""; if (chars[i] === "?") { - throw new TypeError(`Pattern cannot start with "?" at ${i}`); + throw new TypeError( + `Pattern cannot start with "?" at ${i}: ${DEBUG_URL}`, + ); } while (i < chars.length) { @@ -204,15 +206,22 @@ function lexer(str: string) { } else if (chars[i] === "(") { count++; if (chars[i + 1] !== "?") { - throw new TypeError(`Capturing groups are not allowed at ${i}`); + throw new TypeError( + `Capturing groups are not allowed at ${i}: ${DEBUG_URL}`, + ); } } pattern += chars[i++]; } - if (count) throw new TypeError(`Unbalanced pattern at ${pos}`); - if (!pattern) throw new TypeError(`Missing pattern at ${pos}`); + if (count) { + throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`); + } + + if (!pattern) { + throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`); + } tokens.push({ type: "PATTERN", index: i, value: pattern }); continue; From 208cc83c58e90081f9255194d70ba1d4eb4f1c8e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:31:21 -0700 Subject: [PATCH 41/55] Refactor params to match up to next token --- Readme.md | 194 ++++--- scripts/redos.ts | 52 +- src/cases.spec.ts | 1363 ++++++++++----------------------------------- src/index.spec.ts | 89 +-- src/index.ts | 310 ++++------- 5 files changed, 584 insertions(+), 1424 deletions(-) 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("|")}).)`; } From 6b7a4525b4d8633b6520987738ba04a732e1d6b2 Mon Sep 17 00:00:00 2001 From: Gregorio Litenstein Date: Wed, 24 Jul 2024 22:17:56 -0400 Subject: [PATCH 42/55] Throw on unmatched closing paren (#286) Co-authored-by: Blake Embrey --- src/index.spec.ts | 12 ++++++++++++ src/index.ts | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/index.spec.ts b/src/index.spec.ts index 73f2c78..b51dd9b 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -31,6 +31,18 @@ describe("path-to-regexp", () => { ); }); + it("should throw on unmatched )", function () { + expect(() => match("/:fooab)c")).toThrow( + new TypeError("Unmatched ) at 7: https://git.new/pathToRegexpError"), + ); + }); + + it("should throw on unmatched ) after other patterns", function () { + expect(() => match("/:test(\\w+)/:foo(\\d+))")).toThrow( + new TypeError("Unmatched ) at 21: https://git.new/pathToRegexpError"), + ); + }); + it("should throw on missing pattern", () => { expect(() => match("/:foo()")).toThrow( new TypeError( diff --git a/src/index.ts b/src/index.ts index ec4fad1..0d8bc53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,6 +199,10 @@ function lexer(str: string) { continue; } + if (value === ")") { + throw new TypeError(`Unmatched ) at ${i}: ${DEBUG_URL}`); + } + tokens.push({ type: "CHAR", index: i, value: chars[i++] }); } From 341f8737012c7be48d1d928ad34c7d6d1aa47984 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 19:28:32 -0700 Subject: [PATCH 43/55] Test for double decoding --- src/cases.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cases.spec.ts b/src/cases.spec.ts index a837ea4..0f1cbf3 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -299,6 +299,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ params: { test: ";,:@&=+$-_.!~*()" }, }, }, + { + input: "/param%2523", + matches: ["/param%2523", "param%2523"], + expected: { + path: "/param%2523", + params: { test: "param%23" }, + }, + }, ], }, From 0d20b150bb7ba7a3ecf1b993188a2584572326b9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 20:15:30 -0700 Subject: [PATCH 44/55] Split using a string --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0d8bc53..447228d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -506,8 +506,7 @@ export function $match

( const decoders = re.keys.map((key) => { if (decode && (key.modifier === "+" || key.modifier === "*")) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; - const re = new RegExp(escape(separator), "g"); - return (value: string) => value.split(re).map(decode); + return (value: string) => value.split(separator).map(decode); } return decode || NOOP_VALUE; From 26551ecd97a63299e58e27f73ba3535195300034 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 31 Jul 2024 15:17:18 -0700 Subject: [PATCH 45/55] Code gen smaller and faster regexps --- src/index.ts | 62 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index 447228d..9826439 100644 --- a/src/index.ts +++ b/src/index.ts @@ -297,7 +297,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - pattern: `${negate(escape(delimiter))}*`, + pattern: `${negate(delimiter)}*`, modifier: "*", separator: delimiter, }); @@ -590,7 +590,6 @@ function tokensToRegexp(data: TokenData, options: PathOptions) { * Convert a token into a regexp string (re-used for path validation). */ function toRegExpSource(data: TokenData, keys: Key[]): string[] { - const delim = escape(data.delimiter); const sources = Array(data.tokens.length); let backtrack = ""; @@ -600,7 +599,8 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { const token = data.tokens[i]; if (typeof token === "string") { - sources[i] = backtrack = escape(token); + backtrack = token; + sources[i] = escape(token); continue; } @@ -615,32 +615,40 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { const post = escape(suffix); if (token.name) { - let pattern = token.pattern || ""; - + backtrack = suffix || backtrack; keys.unshift(token); if (isRepeat(token)) { - const mod = modifier === "*" ? "?" : ""; - const sep = escape(separator); - - if (!sep) { + if (!separator) { throw new TypeError( `Missing separator for "${token.name}": ${DEBUG_URL}`, ); } - pattern ||= `${negate(delim, sep, post || backtrack)}+`; - sources[i] = - `(?:${pre}((?:${pattern})(?:${sep}(?:${pattern}))*)${post})${mod}`; + const mod = modifier === "*" ? "?" : ""; + const sep = escape(separator); + const pattern = + token.pattern || `${negate(data.delimiter, separator, backtrack)}+`; + + sources[i] = wrap( + pre, + `(?:${pattern})(?:${sep}(?:${pattern}))*`, + post, + mod, + ); } else { - pattern ||= `${negate(delim, post || backtrack)}+`; - sources[i] = `(?:${pre}(${pattern})${post})${modifier}`; + sources[i] = wrap( + pre, + token.pattern || `${negate(data.delimiter, backtrack)}+`, + post, + modifier, + ); } - backtrack = pre || pattern; + backtrack = prefix; } else { sources[i] = `(?:${pre}${post})${modifier}`; - backtrack = `${pre}${post}`; + backtrack = `${prefix}${suffix}`; } } @@ -648,6 +656,24 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { } function negate(...args: string[]) { - const values = Array.from(new Set(args)).filter(Boolean); - return `(?:(?!${values.join("|")}).)`; + const values = args.sort().filter((value, index, array) => { + for (let i = 0; i < index; i++) { + const v = array[i]; + if (v.length && value.startsWith(v)) return false; + } + return value.length > 0; + }); + + const isSimple = values.every((value) => value.length === 1); + if (isSimple) return `[^${escape(values.join(""))}]`; + + return `(?:(?!${values.map(escape).join("|")}).)`; +} + +function wrap(pre: string, pattern: string, post: string, modifier: string) { + if (pre || post) { + return `(?:${pre}(${pattern})${post})${modifier}`; + } + + return `(${pattern})${modifier}`; } From fb4d11d38f81e336a73a2ca0887e8105dd6d72ca Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 31 Jul 2024 15:20:52 -0700 Subject: [PATCH 46/55] Remove matches from tests --- scripts/redos.ts | 2 +- src/cases.spec.ts | 349 +++++++++------------------------------------- src/index.spec.ts | 4 +- src/index.ts | 73 ++++------ 4 files changed, 98 insertions(+), 330 deletions(-) diff --git a/scripts/redos.ts b/scripts/redos.ts index fe2c7ac..f83e52d 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -14,7 +14,7 @@ const TESTS = new Set(MATCH_TESTS.map((test) => test.path)); // ]; for (const path of TESTS) { - const { re } = match(path); + const { re } = match(path) as any; const result = checkSync(re.source, re.flags); if (result.status === "safe") { safe++; diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 0f1cbf3..508b946 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -27,7 +27,6 @@ export interface MatchTestSet { options?: MatchOptions & ParseOptions; tests: Array<{ input: string; - matches: (string | undefined)[] | null; expected: Match; }>; } @@ -212,10 +211,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/"], expected: { path: "/", params: {} }, }, - { input: "/route", matches: ["/"], expected: false }, + { input: "/route", expected: false }, ], }, { @@ -223,14 +221,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, - { input: "/route", matches: null, expected: false }, - { input: "/test/route", matches: ["/test"], expected: false }, + { input: "/route", expected: false }, + { input: "/test/route", expected: false }, { input: "/test/", - matches: ["/test"], expected: false, }, ], @@ -240,14 +236,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test/", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, - { input: "/route", matches: null, expected: false }, - { input: "/test", matches: null, expected: false }, + { input: "/route", expected: false }, + { input: "/test", expected: false }, { input: "/test//", - matches: ["/test/"], expected: false, }, ], @@ -257,17 +251,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route", "route"], expected: false, }, { input: "/route.json", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -275,17 +266,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - matches: ["/route.json", "route.json"], expected: false, }, { input: "/route/test", - matches: ["/route", "route"], expected: false, }, { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { test: "café" }, @@ -293,7 +281,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/;,:@&=+$-_.!~*()", - matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], expected: { path: "/;,:@&=+$-_.!~*()", params: { test: ";,:@&=+$-_.!~*()" }, @@ -301,7 +288,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/param%2523", - matches: ["/param%2523", "param%2523"], expected: { path: "/param%2523", params: { test: "param%23" }, @@ -321,10 +307,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, - { input: "/TEST", matches: null, expected: false }, + { input: "/TEST", expected: false }, ], }, { @@ -335,10 +320,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/TEST", - matches: ["/TEST"], expected: { path: "/TEST", params: {} }, }, - { input: "/test", matches: null, expected: false }, + { input: "/test", expected: false }, ], }, @@ -353,32 +337,26 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/test/", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/test////", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/route/test", - matches: null, expected: false, }, { input: "/test/route", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/route", - matches: null, expected: false, }, ], @@ -391,27 +369,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test/", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, { input: "/test//", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: ["/test/"], expected: false, }, { input: "/route/test/deep", - matches: null, expected: false, }, ], @@ -424,17 +397,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/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" }, @@ -442,7 +412,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -450,12 +419,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/test", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json/test", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -463,7 +430,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { test: "café" }, @@ -479,27 +445,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: null, expected: false, }, { input: "/route/", - matches: ["/route/", "route"], expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route/test", - matches: ["/route/", "route"], expected: false, }, { input: "/route/test/", - matches: ["/route/", "route"], expected: false, }, { input: "/route//test", - matches: ["/route/", "route"], expected: { path: "/route/", params: { test: "route" } }, }, ], @@ -512,27 +473,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: [""], expected: { path: "", params: {} }, }, { input: "/", - matches: [""], expected: { path: "", params: {} }, }, { input: "route", - matches: [""], expected: false, }, { input: "/route", - matches: [""], expected: { path: "", params: {} }, }, { input: "/route/", - matches: [""], expected: { path: "", params: {} }, }, ], @@ -546,17 +502,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "", - matches: ["", undefined], expected: { path: "", params: {} }, }, { input: "/", - matches: ["", undefined], expected: false, }, ], @@ -566,17 +519,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/bar", - matches: ["/bar", undefined], expected: { path: "/bar", params: {} }, }, { input: "/foo/bar", - matches: ["/foo/bar", "foo"], expected: { path: "/foo/bar", params: { test: "foo" } }, }, { input: "/foo/bar/", - matches: ["/foo/bar", "foo"], expected: false, }, ], @@ -586,17 +536,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: ["-bar", undefined], expected: { path: "-bar", params: {} }, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar", "foo"], expected: false, }, ], @@ -606,17 +553,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/-bar", - matches: ["/-bar", undefined], expected: { path: "/-bar", params: {} }, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar", "foo"], expected: false, }, ], @@ -630,22 +574,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "//", - matches: ["", undefined], expected: false, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", - matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", params: { test: ["some", "basic", "route"] }, @@ -658,22 +598,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: ["-bar", undefined], expected: { path: "-bar", params: {} }, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", params: { test: ["foo", "baz"] }, @@ -690,22 +626,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: null, expected: false, }, { input: "//", - matches: null, expected: false, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", - matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", params: { test: ["some", "basic", "route"] }, @@ -718,22 +650,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: null, expected: false, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", params: { test: ["foo", "baz"] }, @@ -750,17 +678,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, ], @@ -770,27 +695,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: null, expected: false, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/abc-bar", - matches: null, expected: false, }, { input: "/123-bar", - matches: ["/123-bar", "123"], expected: { path: "/123-bar", params: { test: "123" } }, }, { input: "/123/456-bar", - matches: null, expected: false, }, ], @@ -800,17 +720,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", ""], expected: { path: "/", params: { test: "" } }, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/123", - matches: ["/route/123", "route/123"], expected: { path: "/route/123", params: { test: "route/123" }, @@ -818,7 +735,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/;,:@&=/+$-_.!/~*()", - matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], expected: { path: "/;,:@&=/+$-_.!/~*()", params: { test: ";,:@&=/+$-_.!/~*()" }, @@ -831,17 +747,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc"], expected: { path: "/abc", params: { test: "abc" } }, }, { input: "/123", - matches: null, expected: false, }, { input: "/abc/123", - matches: ["/abc", "abc"], expected: false, }, ], @@ -851,17 +764,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/this", - matches: ["/this", "this"], expected: { path: "/this", params: { test: "this" } }, }, { input: "/that", - matches: ["/that", "that"], expected: { path: "/that", params: { test: "that" } }, }, { input: "/foo", - matches: null, expected: false, }, ], @@ -871,17 +781,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "/abc", - matches: ["/abc", "abc"], expected: { path: "/abc", params: { test: ["abc"] } }, }, { input: "/abc/abc", - matches: ["/abc/abc", "abc/abc"], expected: { path: "/abc/abc", params: { test: ["abc", "abc"] }, @@ -889,7 +796,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/xyz/xyz", - matches: ["/xyz/xyz", "xyz/xyz"], expected: { path: "/xyz/xyz", params: { test: ["xyz", "xyz"] }, @@ -897,7 +803,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/xyz", - matches: ["/abc/xyz", "abc/xyz"], expected: { path: "/abc/xyz", params: { test: ["abc", "xyz"] }, @@ -905,7 +810,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/xyz/abc/xyz", - matches: ["/abc/xyz/abc/xyz", "abc/xyz/abc/xyz"], expected: { path: "/abc/xyz/abc/xyz", params: { test: ["abc", "xyz", "abc", "xyz"] }, @@ -913,7 +817,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/xyzxyz", - matches: ["/xyz", "xyz"], expected: false, }, ], @@ -927,12 +830,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "test", - matches: ["test"], expected: { path: "test", params: {} }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -942,17 +843,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "route", - matches: ["route", "route"], expected: { path: "route", params: { test: "route" } }, }, { input: "/route", - matches: null, expected: false, }, { input: "route/", - matches: ["route", "route"], expected: false, }, ], @@ -962,12 +860,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "test", - matches: ["test", "test"], expected: { path: "test", params: { test: "test" } }, }, { input: "", - matches: ["", undefined], expected: { path: "", params: {} }, }, ], @@ -977,22 +873,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "route/", - matches: ["route/", "route"], expected: { path: "route/", params: { test: ["route"] } }, }, { input: "/route", - matches: null, expected: false, }, { input: "", - matches: null, expected: false, }, { input: "foo/bar/", - matches: ["foo/bar/", "foo/bar"], expected: { path: "foo/bar/", params: { test: ["foo", "bar"] }, @@ -1009,12 +901,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.json", - matches: ["/test.json"], expected: { path: "/test.json", params: {} }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -1024,22 +914,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/.json", - matches: null, expected: false, }, { input: "/test.json", - matches: ["/test.json", "test"], expected: { path: "/test.json", params: { test: "test" } }, }, { input: "/route.json", - matches: ["/route.json", "route"], expected: { path: "/route.json", params: { test: "route" } }, }, { input: "/route.json.json", - matches: ["/route.json", "route"], expected: false, }, ], @@ -1049,7 +935,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.json.json", - matches: ["/route.json.json", "route.json"], expected: { path: "/route.json.json", params: { test: "route.json" }, @@ -1066,12 +951,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: "html" } }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -1081,7 +964,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.html.json", - matches: ["/test.html.json", "html", "json"], expected: { path: "/test.html.json", params: { format: "json" }, @@ -1089,7 +971,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.html", - matches: null, expected: false, }, ], @@ -1099,12 +980,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test", undefined], expected: { path: "/test", params: { format: undefined } }, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: "html" } }, }, ], @@ -1114,12 +993,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: ["html"] }, @@ -1127,7 +1004,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.html.json", - matches: ["/test.html.json", "html.json"], expected: { path: "/test.html.json", params: { format: ["html", "json"] }, @@ -1140,12 +1016,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: ["html"] }, @@ -1153,7 +1027,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.hbs.html", - matches: ["/test.hbs.html", "hbs.html"], expected: { path: "/test.hbs.html", params: { format: ["hbs", "html"] }, @@ -1170,7 +1043,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.html", - matches: ["/route.html", "route", "html"], expected: { path: "/route.html", params: { test: "route", format: "html" }, @@ -1178,12 +1050,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route", - matches: null, expected: false, }, { input: "/route.html.json", - matches: ["/route.html.json", "route", "html.json"], expected: { path: "/route.html.json", params: { test: "route", format: "html.json" }, @@ -1196,12 +1066,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route", undefined], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json", - matches: ["/route.json", "route", "json"], expected: { path: "/route.json", params: { test: "route", format: "json" }, @@ -1209,7 +1077,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json.html", - matches: ["/route.json.html", "route", "json.html"], expected: { path: "/route.json.html", params: { test: "route", format: "json.html" }, @@ -1222,7 +1089,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.htmlz", - matches: ["/route.htmlz", "route", "html"], expected: { path: "/route.htmlz", params: { test: "route", format: "html" }, @@ -1230,7 +1096,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.html", - matches: null, expected: false, }, ], @@ -1244,17 +1109,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { "0": "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, ], @@ -1264,12 +1126,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { "0": "123" } }, }, ], @@ -1279,7 +1139,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route(\\123\\)", - matches: ["/route(\\123\\)", "123\\"], expected: { path: "/route(\\123\\)", params: { "0": "123\\" }, @@ -1287,7 +1146,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route(\\123)", - matches: null, expected: false, }, ], @@ -1297,22 +1155,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: [""], expected: { path: "", params: {} }, }, { input: "/", - matches: [""], expected: false, }, { input: "/foo", - matches: [""], expected: false, }, { input: "/route", - matches: ["/route"], expected: { path: "/route", params: {} }, }, ], @@ -1322,12 +1176,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", ""], expected: { path: "/", params: { "0": "" } }, }, { input: "/login", - matches: ["/login", "login"], expected: { path: "/login", params: { "0": "login" } }, }, ], @@ -1341,12 +1193,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/testing", - matches: null, expected: false, }, { input: "/(testing)", - matches: ["/(testing)"], expected: { path: "/(testing)", params: {} }, }, ], @@ -1356,7 +1206,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/.+*?{}=^!:$[]|", - matches: ["/.+*?{}=^!:$[]|"], expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, ], @@ -1366,12 +1215,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test/u123", - matches: ["/test/u123", "u123", undefined], expected: { path: "/test/u123", params: { uid: "u123" } }, }, { input: "/test/c123", - matches: ["/test/c123", undefined, "c123"], expected: { path: "/test/c123", params: { cid: "c123" } }, }, ], @@ -1385,12 +1232,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/icon-240.png", - matches: ["/icon-240.png", "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", params: { res: "240" }, @@ -1407,7 +1252,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/match/route", - matches: ["/match/route", "match", "route"], expected: { path: "/match/route", params: { foo: "match", bar: "route" }, @@ -1420,12 +1264,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo(test)/bar", - matches: ["/foo(test)/bar", "foo"], expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { input: "/foo/bar", - matches: null, expected: false, }, ], @@ -1435,7 +1277,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/endpoint/user", - matches: ["/endpoint/user", "endpoint", "user"], expected: { path: "/endpoint/user", params: { remote: "endpoint", user: "user" }, @@ -1443,7 +1284,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/endpoint/user-name", - matches: ["/endpoint/user-name", "endpoint", "user-name"], expected: { path: "/endpoint/user-name", params: { remote: "endpoint", user: "user-name" }, @@ -1451,7 +1291,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo.bar/user-name", - matches: ["/foo.bar/user-name", "foo.bar", "user-name"], expected: { path: "/foo.bar/user-name", params: { remote: "foo.bar", user: "user-name" }, @@ -1464,12 +1303,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route?", - matches: ["/route?", "route"], expected: { path: "/route?", params: { foo: "route" } }, }, { input: "/route", - matches: null, expected: false, }, ], @@ -1479,17 +1316,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foobar", - matches: ["/foobar", "foo"], expected: { path: "/foobar", params: { foo: ["foo"] } }, }, { input: "/foo/bar", - matches: null, expected: false, }, { input: "/foo/barbar", - matches: null, expected: false, }, ], @@ -1499,12 +1333,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foobaz", - matches: ["/foobaz", "foo"], expected: { path: "/foobaz", params: { pre: "foo" } }, }, { input: "/baz", - matches: ["/baz", undefined], expected: { path: "/baz", params: { pre: undefined } }, }, ], @@ -1514,7 +1346,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello(world)", - matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", params: { foo: "hello", bar: "world" }, @@ -1522,7 +1353,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello()", - matches: null, expected: false, }, ], @@ -1532,7 +1362,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello(world)", - matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", params: { foo: "hello", bar: "world" }, @@ -1540,7 +1369,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello()", - matches: ["/hello()", "hello", undefined], expected: { path: "/hello()", params: { foo: "hello", bar: undefined }, @@ -1553,12 +1381,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/video", - matches: ["/video", "video", undefined], expected: { path: "/video", params: { postType: "video" } }, }, { input: "/video+test", - matches: ["/video+test", "video", "+test"], expected: { path: "/video+test", params: { 0: "+test", postType: "video" }, @@ -1566,7 +1392,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/video+", - matches: ["/video", "video", undefined], expected: false, }, ], @@ -1576,12 +1401,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/-ext", - matches: null, expected: false, }, { input: "-ext", - matches: ["-ext", undefined, undefined], expected: { path: "-ext", params: { foo: undefined, bar: undefined }, @@ -1589,12 +1412,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-ext", - matches: ["/foo-ext", "foo", undefined], expected: { path: "/foo-ext", params: { foo: "foo" } }, }, { input: "/foo/bar-ext", - matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", params: { foo: "foo", bar: "bar" }, @@ -1602,7 +1423,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/-ext", - matches: null, expected: false, }, ], @@ -1612,12 +1432,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo-ext", - matches: ["/foo-ext", "foo", undefined], expected: { path: "/foo-ext", params: { required: "foo" } }, }, { input: "/foo/bar-ext", - matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", params: { required: "foo", optional: "bar" }, @@ -1625,7 +1443,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/-ext", - matches: null, expected: false, }, ], @@ -1639,7 +1456,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/café", - matches: ["/café", "café"], expected: { path: "/café", params: { foo: "café" } }, }, ], @@ -1652,7 +1468,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { foo: "caf%C3%A9" }, @@ -1665,7 +1480,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/café", - matches: ["/café"], expected: { path: "/café", params: {} }, }, ], @@ -1678,7 +1492,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/caf%C3%A9", - matches: ["/caf%C3%A9"], expected: { path: "/caf%C3%A9", params: {} }, }, ], @@ -1695,7 +1508,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "example.com", - matches: ["example.com", "example"], expected: { path: "example.com", params: { domain: "example" }, @@ -1703,7 +1515,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "github.com", - matches: ["github.com", "github"], expected: { path: "github.com", params: { domain: "github" }, @@ -1719,7 +1530,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "mail.example.com", - matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", params: { domain: "example" }, @@ -1727,7 +1537,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "mail.github.com", - matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", params: { domain: "github" }, @@ -1743,12 +1552,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "mail.com", - matches: ["mail.com", undefined], expected: { path: "mail.com", params: { domain: undefined } }, }, { input: "mail.example.com", - matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", params: { domain: "example" }, @@ -1756,7 +1563,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "mail.github.com", - matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", params: { domain: "github" }, @@ -1772,12 +1578,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "example.com", - matches: ["example.com", "com"], expected: { path: "example.com", params: { ext: "com" } }, }, { input: "example.org", - matches: ["example.org", "org"], expected: { path: "example.org", params: { ext: "org" } }, }, ], @@ -1791,12 +1595,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "this is a test", - matches: ["this is"], expected: { path: "this is", params: {} }, }, { input: "this isn't", - matches: ["this is"], expected: false, }, ], @@ -1810,12 +1612,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "$x", - matches: ["$x", "x", undefined], expected: { path: "$x", params: { foo: "x" } }, }, { input: "$x$y", - matches: ["$x$y", "x", "y"], expected: { path: "$x$y", params: { foo: "x", bar: "y" } }, }, ], @@ -1825,12 +1625,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "$x", - matches: ["$x", "x"], expected: { path: "$x", params: { foo: ["x"] } }, }, { input: "$x$y", - matches: ["$x$y", "x$y"], expected: { path: "$x$y", params: { foo: ["x", "y"] } }, }, ], @@ -1840,12 +1638,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "name", - matches: ["name", undefined, undefined, undefined], expected: { path: "name", params: {} }, }, { input: "name/test", - matches: ["name/test", "test", undefined, undefined], expected: { path: "name/test", params: { attr1: "test" }, @@ -1853,7 +1649,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1", - matches: ["name/1", "1", undefined, undefined], expected: { path: "name/1", params: { attr1: "1" }, @@ -1861,7 +1656,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2", - matches: ["name/1-2", "1", "2", undefined], expected: { path: "name/1-2", params: { attr1: "1", attr2: "2" }, @@ -1869,7 +1663,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2-3", - matches: ["name/1-2-3", "1", "2", "3"], expected: { path: "name/1-2-3", params: { attr1: "1", attr2: "2", attr3: "3" }, @@ -1877,12 +1670,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/foo-bar/route", - matches: ["name/foo-bar", "foo", "bar", undefined], expected: false, }, { input: "name/test/route", - matches: ["name/test", "test", undefined, undefined], expected: false, }, ], @@ -1892,12 +1683,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "name", - matches: ["name", undefined], expected: { path: "name", params: {} }, }, { input: "name/1", - matches: ["name/1", "1"], expected: { path: "name/1", params: { attrs: ["1"] }, @@ -1905,7 +1694,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2", - matches: ["name/1-2", "1-2"], expected: { path: "name/1-2", params: { attrs: ["1", "2"] }, @@ -1913,7 +1701,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2-3", - matches: ["name/1-2-3", "1-2-3"], expected: { path: "name/1-2-3", params: { attrs: ["1", "2", "3"] }, @@ -1921,12 +1708,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/foo-bar/route", - matches: ["name/foo-bar", "foo-bar"], expected: false, }, { input: "name/test/route", - matches: ["name/test", "test"], expected: false, }, ], @@ -1940,27 +1725,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, { input: "/123.123", - matches: ["/123.123", "123.123"], expected: { path: "/123.123", params: { test: "123.123" } }, }, { input: "/123.abc", - matches: ["/123", "123"], expected: false, }, ], @@ -1970,12 +1750,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/login", - matches: null, expected: false, }, ], @@ -1989,12 +1767,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/user/123", - matches: ["/user/123", undefined, "123"], expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", - matches: ["/users/123", "s", "123"], expected: { path: "/users/123", params: { 0: "s", user: "123" }, @@ -2007,12 +1783,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/user/123", - matches: ["/user/123", "123"], expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", - matches: ["/users/123", "123"], expected: { path: "/users/123", params: { user: "123" } }, }, ], @@ -2026,7 +1800,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/files/hello/world.txt", - matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", params: { path: ["hello", "world"], ext: ["txt"] }, @@ -2034,7 +1807,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/hello/world.txt.png", - matches: ["/files/hello/world.txt.png", "hello/world", "txt.png"], expected: { path: "/files/hello/world.txt.png", params: { path: ["hello", "world"], ext: ["txt", "png"] }, @@ -2042,7 +1814,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg", "my/photo", "jpg"], expected: false, }, ], @@ -2052,7 +1823,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/files/hello/world.txt", - matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", params: { path: ["hello", "world"], ext: "txt" }, @@ -2060,7 +1830,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg", "my/photo", "jpg"], expected: false, }, ], @@ -2070,7 +1839,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "#/", - matches: ["#/", undefined], expected: { path: "#/", params: {} }, }, ], @@ -2080,7 +1848,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo/test1/test2", - matches: ["/foo/test1/test2", "test1/test2"], expected: { path: "/foo/test1/test2", params: { bar: ["test1", "test2"] }, @@ -2093,12 +1860,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/entity/foo", - matches: null, expected: false, }, { input: "/entity/foo/", - matches: ["/entity/foo/", "foo", undefined], expected: { path: "/entity/foo/", params: { id: "foo" } }, }, ], @@ -2108,22 +1873,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test/", - matches: ["/test/", undefined], expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: ["/test/route", "route"], expected: { path: "/test/route", params: { "0": ["route"] } }, }, { input: "/test/route/nested", - matches: ["/test/route/nested", "route/nested"], expected: { path: "/test/route/nested", params: { "0": ["route", "nested"] }, @@ -2140,17 +1901,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], expected: { path: "/", params: { "0": undefined } }, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { "0": ["route"] } }, }, { input: "/route/nested", - matches: ["/route/nested", "route/nested"], expected: { path: "/route/nested", params: { "0": ["route", "nested"] }, @@ -2163,12 +1921,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "/"], expected: { path: "/", params: { "0": ["", ""] } }, }, { input: "/test", - matches: ["/test", "/test"], expected: { path: "/test", params: { "0": ["", "test"] } }, }, ], @@ -2179,16 +1935,72 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "/"], expected: { path: "/", params: { "0": "/" } }, }, { input: "/test", - matches: ["/test", "/test"], expected: { path: "/test", params: { "0": "/test" } }, }, ], }, + { + path: "/*.:ext", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test"], ext: "html" }, + }, + }, + { + input: "/test.html/nested", + expected: false, + }, + ], + }, + { + path: "/*{.:ext}?", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test.html"], ext: undefined }, + }, + }, + { + input: "/test.html/nested", + expected: { + params: { + "0": ["test.html", "nested"], + }, + path: "/test.html/nested", + }, + }, + ], + }, + { + path: "/*{.:ext}*", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test.html"], ext: undefined }, + }, + }, + { + input: "/test.html/nested", + expected: { + params: { + "0": ["test.html", "nested"], + }, + path: "/test.html/nested", + }, + }, + ], + }, /** * Longer prefix. @@ -2198,12 +2010,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route", undefined], expected: { path: "/route", params: { foo: "route" } }, }, { input: "/route/test/again", - matches: ["/route/test/again", "route", "again"], expected: { path: "/route/test/again", params: { foo: "route", bar: "again" }, @@ -2220,12 +2030,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "test"], expected: { path: "/", params: { foo: ["test"] } }, }, { input: "/", - matches: ["/", "test>", params: { foo: ["test", "again"] }, @@ -2242,12 +2050,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: ["", undefined, undefined], expected: { path: "", params: {} }, }, { input: "test/", - matches: ["test/", "test", undefined], expected: { path: "test/", params: { foo: "test" }, @@ -2255,7 +2061,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "a/b.", - matches: ["a/b.", "a", "b"], expected: { path: "a/b.", params: { foo: "a", bar: "b" } }, }, ], @@ -2265,22 +2070,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", undefined], expected: { path: "/abc", params: {} }, }, { input: "/abcabc", - matches: ["/abc", undefined], expected: false, }, { input: "/abcabc123", - matches: ["/abcabc123", "123"], expected: { path: "/abcabc123", params: { foo: "123" } }, }, { input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc123"], expected: { path: "/abcabcabc123", params: { foo: "abc123" }, @@ -2288,7 +2089,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabcabc", - matches: ["/abcabcabc", "abc"], expected: { path: "/abcabcabc", params: { foo: "abc" } }, }, ], @@ -2298,22 +2098,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: null, expected: false, }, { input: "/abcabc", - matches: null, expected: false, }, { input: "/abcabc123", - matches: null, expected: false, }, { input: "/acb", - matches: ["/acb", "acb", undefined], expected: { path: "/acb", params: { foo: "acb" }, @@ -2321,7 +2117,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/acbabc123", - matches: ["/acbabc123", "acb", "123"], expected: { path: "/acbabc123", params: { foo: "acb", bar: "123" }, @@ -2334,17 +2129,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: null, expected: false, }, { input: "/abcabc", - matches: null, expected: false, }, { input: "/abcabc123", - matches: null, expected: false, }, ], @@ -2354,12 +2146,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc", undefined], expected: { path: "/abc", params: { foo: "abc" } }, }, { input: "/abc.txt", - matches: ["/abc.txt", "abc.txt", undefined], expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, }, ], @@ -2369,7 +2159,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route|world|", - matches: ["/route|world|", "world"], expected: { path: "/route|world|", params: { param: "world" }, @@ -2377,7 +2166,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route||", - matches: null, expected: false, }, ], @@ -2387,7 +2175,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello|world|", - matches: ["/hello|world|", "hello", "world"], expected: { path: "/hello|world|", params: { foo: "hello", bar: "world" }, @@ -2395,7 +2182,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello||", - matches: null, expected: false, }, ], @@ -2405,12 +2191,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "x@y", - matches: ["x@y", "x", "y"], expected: { path: "x@y", params: { foo: "x", bar: "y" } }, }, { input: "x@", - matches: null, expected: false, }, ], @@ -2427,12 +2211,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "%25hello", - matches: ["%25hello", "hello", undefined], expected: { path: "%25hello", params: { foo: "hello" } }, }, { input: "%25hello%25world", - matches: ["%25hello%25world", "hello", "world"], expected: { path: "%25hello%25world", params: { foo: "hello", bar: "world" }, @@ -2440,7 +2222,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "%25555%25222", - matches: ["%25555%25222", "555", "222"], expected: { path: "%25555%25222", params: { foo: "555", bar: "222" }, diff --git a/src/index.spec.ts b/src/index.spec.ts index b51dd9b..ef019c9 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -120,10 +120,8 @@ describe("path-to-regexp", () => { describe.each(MATCH_TESTS)( "match $path with $options", ({ path, options, tests }) => { - it.each(tests)("should match $input", ({ input, matches, expected }) => { + it.each(tests)("should match $input", ({ input, expected }) => { const fn = match(path, options); - - expect(exec(fn.re, input)).toEqual(matches); expect(fn(input)).toEqual(expected); }); }, diff --git a/src/index.ts b/src/index.ts index 9826439..2692df0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -365,23 +365,23 @@ export function isOptional(key: Key) { * Convert a single token into a path building function. */ function keyToFunction( - token: Key, + key: Key, encode: Encode | false, ): (data: ParamData) => string { const encodeValue = encode || NOOP_VALUE; - const { prefix = "", suffix = "", separator = suffix + prefix } = token; + const { prefix = "", suffix = "", separator = suffix + prefix } = key; - if (encode && isRepeat(token)) { + if (encode && isRepeat(key)) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { - throw new TypeError(`Expected "${token.name}/${index}" to be a string`); + throw new TypeError(`Expected "${key.name}/${index}" to be a string`); } return encodeValue(value); }; const compile = (value: unknown) => { if (!Array.isArray(value)) { - throw new TypeError(`Expected "${token.name}" to be an array`); + throw new TypeError(`Expected "${key.name}" to be an array`); } if (value.length === 0) return ""; @@ -389,37 +389,37 @@ function keyToFunction( return prefix + value.map(stringify).join(separator) + suffix; }; - if (isOptional(token)) { + if (isOptional(key)) { return (data): string => { - const value = data[token.name]; + const value = data[key.name]; if (value == null) return ""; return value.length ? compile(value) : ""; }; } return (data): string => { - const value = data[token.name]; + const value = data[key.name]; return compile(value); }; } const stringify = (value: unknown) => { if (typeof value !== "string") { - throw new TypeError(`Expected "${token.name}" to be a string`); + throw new TypeError(`Expected "${key.name}" to be a string`); } return prefix + encodeValue(value) + suffix; }; - if (isOptional(token)) { + if (isOptional(key)) { return (data): string => { - const value = data[token.name]; + const value = data[key.name]; if (value == null) return ""; return stringify(value); }; } return (data): string => { - const value = data[token.name]; + const value = data[key.name]; return stringify(value); }; } @@ -480,18 +480,7 @@ export type Match

= false | MatchResult

; /** * The match function takes a string and returns whether it matched the path. */ -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 - ); - }; +export type MatchFunction

= (path: string) => Match

; /** * Create path match function from `path-to-regexp` spec. @@ -501,18 +490,29 @@ export function $match

( options: MatchOptions = {}, ): MatchFunction

{ const { decode = decodeURIComponent, end = true } = options; - const re = tokensToRegexp(data, options); + const { delimiter } = data; + const keys: Key[] = []; + const flags = toFlags(options); + const sources = toRegExpSource(data, keys); + const re = new RegExp( + `^${sources.join("")}(?=${escape(delimiter)}|$)`, + flags, + ); + + const decoders = keys.map((key) => { + if (!decode) return NOOP_VALUE; - const decoders = re.keys.map((key) => { - if (decode && (key.modifier === "+" || key.modifier === "*")) { + if (isRepeat(key)) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; return (value: string) => value.split(separator).map(decode); } - return decode || NOOP_VALUE; + return decode; }); - const validate = end ? isEnd : isDelimiterOrEnd(data.delimiter); + const isValid = end + ? (a: string, b: string) => a.length === b.length + : () => true; return Object.assign( function match(input: string) { @@ -520,13 +520,13 @@ export function $match

( if (!m) return false; const { 0: path } = m; - if (!validate(input, path)) return false; + if (!isValid(input, path)) return false; const params = Object.create(null); for (let i = 1; i < m.length; i++) { if (m[i] === undefined) continue; - const key = re.keys[i - 1]; + const key = keys[i - 1]; const decoder = decoders[i - 1]; params[key.name] = decoder(m[i]); } @@ -575,17 +575,6 @@ export interface Key { */ export type Token = string | Key; -/** - * Expose a function for taking tokens and returning a RegExp. - */ -function tokensToRegexp(data: TokenData, options: PathOptions) { - const flags = toFlags(options); - 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). */ From 74f97b59728f111bd457646a340f7c2eb8cfec66 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 12 Aug 2024 16:20:55 -0700 Subject: [PATCH 47/55] Create SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4aa608f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Security contact information + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. From 60f2121e9b66b7b622cc01080df0aabda9eedee6 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 1 Sep 2024 14:56:05 -0700 Subject: [PATCH 48/55] Rewrite and simplify API --- Readme.md | 231 ++------ package.json | 1 + scripts/redos.ts | 8 +- src/cases.spec.ts | 1365 +++++++++++-------------------------------- src/index.bench.ts | 42 ++ src/index.spec.ts | 174 ++---- src/index.ts | 763 +++++++++++------------- tsconfig.build.json | 2 +- 8 files changed, 819 insertions(+), 1767 deletions(-) create mode 100644 src/index.bench.ts diff --git a/Readme.md b/Readme.md index eaa60b2..263c0f7 100644 --- a/Readme.md +++ b/Readme.md @@ -24,29 +24,9 @@ const { match, compile, parse } = require("path-to-regexp"); // parse(path, options?) ``` -### Match - -The `match` function returns a function for transforming paths into parameters: - -- **path** A string. -- **options** _(optional)_ (See [parse](#parse) for more options) - - **sensitive** Regexp will be case sensitive. (default: `false`) - - **end** Validate the match reaches the end of the string. (default: `true`) - - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) - -```js -const fn = match("/foo/:bar"); -``` - -**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). - ### Parameters -Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. - -#### Named parameters - -Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters, similar to JavaScript. +Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. They are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid JavaScript identifier, or be double quoted to use other characters (`:"param-name"`). ```js const fn = match("/:foo/:bar"); @@ -55,137 +35,54 @@ fn("/test/route"); //=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` -##### Custom matching parameters - -Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: - -```js -const exampleNumbers = match("/icon-:foo(\\d+).png"); - -exampleNumbers("/icon-123.png"); -//=> { path: '/icon-123.png', params: { foo: '123' } } - -exampleNumbers("/icon-abc.png"); -//=> false - -const exampleWord = pathToRegexp("/(user|u)"); - -exampleWord("/u"); -//=> { path: '/u', params: { '0': 'u' } } - -exampleWord("/users"); -//=> false -``` - -**Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. - -#### Unnamed parameters - -It is possible to define a parameter without a name. The name will be numerically indexed: - -```js -const fn = match("/:foo/(.*)"); - -fn("/test/route"); -//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } } -``` - -#### Custom prefix and suffix - -Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: - -```js -const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?"); - -fn("/test"); -//=> { path: '/test', params: { attr1: 'test' } } - -fn("/test-test"); -//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } } -``` - -#### Modifiers - -Modifiers are used after parameters with custom prefixes and suffixes (`{}`). - -##### Optional - -Parameters can be suffixed with a question mark (`?`) to make the parameter optional. - -```js -const fn = match("/:foo{/:bar}?"); - -fn("/test"); -//=> { path: '/test', params: { foo: 'test' } } - -fn("/test/route"); -//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } -``` - -##### Zero or more +### Wildcard -Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. +Wildcard parameters match one or more characters across multiple segments. They are defined the same way as regular parameters, but are prefixed with an asterisk (`*foo`). ```js -const fn = match("{/:foo}*"); - -fn("/foo"); -//=> { path: '/foo', params: { foo: [ 'foo' ] } } +const fn = match("/*splat"); fn("/bar/baz"); -//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } +//=> { path: '/bar/baz', params: { splat: [ 'bar', 'baz' ] } } ``` -##### One or more +### Optional -Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches. +Braces can be used to define parts of the path that are optional. ```js -const fn = match("{/:foo}+"); +const fn = match("/users{/:id}/delete"); -fn("/"); -//=> false +fn("/users/delete"); +//=> { path: '/users/delete', params: {} } -fn("/bar/baz"); -//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } +fn("/users/123/delete"); +//=> { path: '/users/123/delete', params: { id: '123' } } ``` -##### Custom separator - -By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: - -```js -const fn = match("/name{/:parts;-}+"); +## Match -fn("/name"); -//=> false +The `match` function returns a function for matching strings against a path: -fn("/bar/1-2-3"); -//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } } -``` - -#### Wildcard - -A wildcard is also supported. It is roughly equivalent to `(.*)`. +- **path** String or array of strings. +- **options** _(optional)_ (See [parse](#parse) for more options) + - **sensitive** Regexp will be case sensitive. (default: `false`) + - **end** Validate the match reaches the end of the string. (default: `true`) + - **trailing** Allows optional trailing delimiter to match. (default: `true`) + - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) ```js -const fn = match("/*"); - -fn("/"); -//=> { path: '/', params: {} } - -fn("/bar/baz"); -//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } } +const fn = match("/foo/:bar"); ``` -### Compile ("Reverse" Path-To-RegExp) +**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). + +## Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: - **path** A string. - **options** (See [parse](#parse) for more options) - - **sensitive** Regexp will be case sensitive. (default: `false`) - - **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`) - **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) ```js @@ -194,46 +91,34 @@ const toPath = compile("/user/:id"); toPath({ id: "name" }); //=> "/user/name" toPath({ id: "café" }); //=> "/user/caf%C3%A9" -// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. -const toPathRaw = compile("/user/:id", { encode: false }); - -toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" -toPathRaw({ id: ":/" }); //=> Throws, "/user/:/" when `validate` is `false`. - -const toPathRepeated = compile("{/:segment}+"); +const toPathRepeated = compile("/*segment"); toPathRepeated({ segment: ["foo"] }); //=> "/foo" toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c" -const toPathRegexp = compile("/user/:id(\\d+)"); +// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. +const toPathRaw = compile("/user/:id", { encode: false }); -toPathRegexp({ id: "123" }); //=> "/user/123" +toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" ``` ## Developers - If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around. -- To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. +- To ensure matches work on paths containing characters usually encoded, such as emoji, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. ### Parse -The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `$match` and `$compile`. +The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `match` and `compile`. - **path** A string. - **options** _(optional)_ - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) + - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) ### Tokens -The `tokens` returned by `TokenData` is an array of strings or keys, represented as objects, with the following properties: - -- `name` The name of the token -- `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`) -- `suffix` _(optional)_ The suffix string for the segment (e.g. `""`) -- `pattern` _(optional)_ The pattern defined to match this token -- `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`) -- `separator` _(optional)_ The string used to separate repeated parameters +`TokenData` is a sequence of tokens, currently of types `text`, `parameter`, `wildcard`, or `group`. ### Custom path @@ -242,9 +127,12 @@ In some applications, you may not be able to use the `path-to-regexp` syntax, bu ```js import { TokenData, match } from "path-to-regexp"; -const tokens = ["/", { name: "foo" }]; -const path = new TokenData(tokens, "/"); -const fn = $match(path); +const tokens = [ + { type: "text", value: "/" }, + { type: "parameter", name: "foo" }, +]; +const path = new TokenData(tokens); +const fn = match(path); fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } ``` @@ -253,55 +141,34 @@ fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } An effort has been made to ensure ambiguous paths from previous releases throw an error. This means you might be seeing an error when things worked before. -### Unexpected `?`, `*`, or `+` - -In previous major versions `/` and `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. For example: - -- `/:key?` → `{/:key}?` or `/:key*` → `{/:key}*` or `/:key+` → `{/:key}+` -- `.:key?` → `{.:key}?` or `.:key*` → `{.:key}*` or `.:key+` → `{.:key}+` -- `:key?` → `{:key}?` or `:key*` → `{:key}*` or `:key+` → `{:key}+` +### Unexpected `?` or `+` -### Unexpected `;` +In past releases, `?`, `*`, and `+` were used to denote optional or repeating parameters. As an alternative, try these: -Used as a [custom separator](#custom-separator) for repeated parameters. +- For optional (`?`), use an empty segment in a group such as `/:file{.:ext}`. +- For repeating (`+`), only wildcard matching is supported, such as `/*path`. +- For optional repeating (`*`), use a group and a wildcard parameter such as `/files{/*path}`. -### Unexpected `!`, `@`, or `,` +### Unexpected `(`, `)`, `[`, `]`, etc. -These characters have been reserved for future use. - -### Missing separator - -Repeated parameters must have a separator to be valid. For example, `{:foo}*` can't be used. Separators can be defined manually, such as `{:foo;/}*`, or they default to the suffix and prefix with the parameter, such as `{/:foo}*`. +Previous versions of Path-to-RegExp used these for RegExp features. This version no longer supports them so they've been reserved to avoid ambiguity. To use these characters literally, escape them with a backslash, e.g. `"\\("`. ### Missing parameter name -Parameter names, the part after `:`, must be a valid JavaScript identifier. For example, it cannot start with a number or dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. +Parameter names, the part after `:` or `*`, must be a valid JavaScript identifier. For example, it cannot start with a number or contain a dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. ### Unterminated quote Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character. -### Pattern cannot start with "?" - -Parameters in `path-to-regexp` must be basic groups. However, you can use features that require the `?` nested within the pattern. For example, `:foo((?!login)[^/]+)` is valid, but `:foo(?!login)` is not. - -### Capturing groups are not allowed - -A parameter pattern can not contain nested capturing groups. - -### Unbalanced or missing pattern - -A parameter pattern must have the expected number of parentheses. An unbalanced amount, such as `((?!login)` implies something has been written that is invalid. Check you didn't forget any parentheses. - ### Express <= 4.x Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways: -- The only part of the string that is a regex is within `()`. - - In Express.js 4.x, everything was passed as-is after a simple replacement, so you could write `/[a-z]+` to match `/test`. -- The `?` optional character must be used after `{}`. +- Regexp characters can no longer be provided. +- The optional character `?` is no longer supported, use braces instead: `/:file{.:ext}`. - Some characters have new meaning or have been reserved (`{}?*+@!;`). -- The parameter name now supports all unicode identifier characters, previously it was only `[a-z0-9]`. +- The parameter name now supports all JavaScript identifier characters, previously it was only `[a-z0-9]`. ## License diff --git a/package.json b/package.json index e1c220a..a57212a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dist/" ], "scripts": { + "bench": "vitest bench", "build": "ts-scripts build", "format": "ts-scripts format", "lint": "ts-scripts lint", diff --git a/scripts/redos.ts b/scripts/redos.ts index f83e52d..841cd07 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -5,13 +5,7 @@ import { MATCH_TESTS } from "../src/cases.spec.js"; let safe = 0; let fail = 0; -const TESTS = new Set(MATCH_TESTS.map((test) => test.path)); -// const TESTS = [ -// ":path([^\\.]+).:ext", -// ":path.:ext(\\w+)", -// ":path{.:ext([^\\.]+)}", -// "/:path.:ext(\\\\w+)", -// ]; +const TESTS = MATCH_TESTS.map((x) => x.path); for (const path of TESTS) { const { re } = match(path) as any; diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 508b946..ef06e1f 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -34,31 +34,56 @@ export interface MatchTestSet { export const PARSER_TESTS: ParserTestSet[] = [ { path: "/", - expected: ["/"], + expected: [{ type: "text", value: "/" }], }, { path: "/:test", - expected: ["/", { name: "test" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + ], }, { path: '/:"0"', - expected: ["/", { name: "0" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "0" }, + ], }, { path: "/:_", - expected: ["/", { name: "_" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "_" }, + ], }, { path: "/:café", - expected: ["/", { name: "café" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "café" }, + ], }, { path: '/:"123"', - expected: ["/", { name: "123" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "123" }, + ], }, { path: '/:"1\\"\\2\\"3"', - expected: ["/", { name: '1"2"3' }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: '1"2"3' }, + ], + }, + { + path: "/*path", + expected: [ + { type: "text", value: "/" }, + { type: "wildcard", name: "path" }, + ], }, ]; @@ -106,7 +131,6 @@ export const COMPILE_TESTS: CompileTestSet[] = [ }, { path: "/:test", - options: { validate: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -116,7 +140,7 @@ export const COMPILE_TESTS: CompileTestSet[] = [ }, { path: "/:test", - options: { validate: false, encode: false }, + options: { encode: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -124,16 +148,6 @@ export const COMPILE_TESTS: CompileTestSet[] = [ { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, - { - path: "/:test", - options: { encode: encodeURIComponent }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, - ], - }, { path: "/:test", options: { encode: () => "static" }, @@ -145,56 +159,32 @@ export const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "{/:test}?", + path: "{/:test}", options: { encode: false }, tests: [ { input: undefined, expected: "" }, { input: {}, expected: "" }, { input: { test: undefined }, expected: "" }, { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, { - path: "/:test(.*)", - options: { encode: false }, + path: "/*test", tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, - { input: { test: "" }, expected: "/" }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123/xyz" }, - ], - }, - { - path: "{/:test}*", - tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: [] }, expected: "" }, - { input: { test: [""] }, expected: null }, + { input: { test: [] }, expected: null }, { input: { test: ["123"] }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, ], }, { - path: "{/:test}*", + path: "/*test", options: { encode: false }, tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: "" }, expected: null }, { input: { test: "123" }, expected: "/123" }, { input: { test: "123/xyz" }, expected: "/123/xyz" }, - { input: { test: ["123", "xyz"] }, expected: null }, - ], - }, - { - path: "/{<:foo>}+", - tests: [ - { input: undefined, expected: null }, - { input: { foo: ["x", "y", "z"] }, expected: "/" }, ], }, ]; @@ -227,7 +217,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/route", expected: false }, { input: "/test/", - expected: false, + expected: { path: "/test/", params: {} }, }, ], }, @@ -242,7 +232,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", expected: false }, { input: "/test//", - expected: false, + expected: { path: "/test//", params: {} }, }, ], }, @@ -255,7 +245,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/", - expected: false, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", @@ -266,7 +256,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - expected: false, + expected: { + path: "/route.json/", + params: { test: "route.json" }, + }, }, { input: "/route/test", @@ -341,7 +334,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test/", - expected: { path: "/test", params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/test////", @@ -377,7 +370,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test//", - expected: { path: "/test/", params: {} }, + expected: { path: "/test//", params: {} }, }, { input: "/test/route", @@ -401,7 +394,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/", - expected: { path: "/route", params: { test: "route" } }, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", @@ -413,7 +406,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route.json/", expected: { - path: "/route.json", + path: "/route.json/", params: { test: "route.json" }, }, }, @@ -477,7 +470,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/", - expected: { path: "", params: {} }, + expected: { path: "/", params: {} }, }, { input: "route", @@ -498,7 +491,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Optional. */ { - path: "{/:test}?", + path: "{/route}", + tests: [ + { + input: "", + expected: { path: "", params: {} }, + }, + { + input: "/", + expected: { path: "/", params: {} }, + }, + { + input: "/foo", + expected: false, + }, + { + input: "/route", + expected: { path: "/route", params: {} }, + }, + ], + }, + { + path: "{/:test}", tests: [ { input: "/route", @@ -510,12 +524,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/", - expected: false, + expected: { path: "/", params: {} }, }, ], }, { - path: "{/:test}?/bar", + path: "{/:test}/bar", tests: [ { input: "/bar", @@ -527,12 +541,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/bar/", - expected: false, + expected: { path: "/foo/bar/", params: { test: "foo" } }, }, ], }, { - path: "{/:test}?-bar", + path: "{/:test}-bar", tests: [ { input: "-bar", @@ -544,12 +558,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-bar/", - expected: false, + expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, { - path: "/{:test}?-bar", + path: "/{:test}-bar", tests: [ { input: "/-bar", @@ -561,810 +575,266 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-bar/", - expected: false, + expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, /** - * Zero or more times. + * No prefix characters. */ { - path: "{/:test}*", + path: "test", tests: [ { - input: "/", - expected: false, + input: "test", + expected: { path: "test", params: {} }, }, { - input: "//", + input: "/test", expected: false, }, - { - input: "/route", - expected: { path: "/route", params: { test: ["route"] } }, - }, - { - input: "/some/basic/route", - expected: { - path: "/some/basic/route", - params: { test: ["some", "basic", "route"] }, - }, - }, ], }, { - path: "{/:test}*-bar", + path: ":test", tests: [ { - input: "-bar", - expected: { path: "-bar", params: {} }, + input: "route", + expected: { path: "route", params: { test: "route" } }, }, { - input: "/-bar", + input: "/route", expected: false, }, { - input: "/foo-bar", - expected: { path: "/foo-bar", params: { test: ["foo"] } }, + input: "route/", + expected: { path: "route/", params: { test: "route" } }, + }, + ], + }, + { + path: "{:test}", + tests: [ + { + input: "test", + expected: { path: "test", params: { test: "test" } }, }, { - input: "/foo/baz-bar", - expected: { - path: "/foo/baz-bar", - params: { test: ["foo", "baz"] }, - }, + input: "", + expected: { path: "", params: {} }, }, ], }, /** - * One or more times. + * Formats. */ { - path: "{/:test}+", + path: "/test.json", tests: [ { - input: "/", - expected: false, + input: "/test.json", + expected: { path: "/test.json", params: {} }, }, { - input: "//", + input: "/test", expected: false, }, - { - input: "/route", - expected: { path: "/route", params: { test: ["route"] } }, - }, - { - input: "/some/basic/route", - expected: { - path: "/some/basic/route", - params: { test: ["some", "basic", "route"] }, - }, - }, ], }, { - path: "{/:test}+-bar", + path: "/:test.json", tests: [ { - input: "-bar", + input: "/.json", expected: false, }, { - input: "/-bar", - expected: false, + input: "/test.json", + expected: { path: "/test.json", params: { test: "test" } }, }, { - input: "/foo-bar", - expected: { path: "/foo-bar", params: { test: ["foo"] } }, + input: "/route.json", + expected: { path: "/route.json", params: { test: "route" } }, }, { - input: "/foo/baz-bar", - expected: { - path: "/foo/baz-bar", - params: { test: ["foo", "baz"] }, - }, + input: "/route.json.json", + expected: { path: "/route.json.json", params: { test: "route.json" } }, }, ], }, /** - * Custom parameters. + * Format and path params. */ { - path: String.raw`/:test(\d+)`, + path: "/:test.:format", tests: [ { - input: "/123", - expected: { path: "/123", params: { test: "123" } }, + input: "/route.html", + expected: { + path: "/route.html", + params: { test: "route", format: "html" }, + }, }, { - input: "/abc", + input: "/route", expected: false, }, { - input: "/123/abc", - expected: false, + input: "/route.html.json", + expected: { + path: "/route.html.json", + params: { test: "route.html", format: "json" }, + }, }, ], }, { - path: String.raw`/:test(\d+)-bar`, + path: "/:test{.:format}", tests: [ { - input: "-bar", - expected: false, + input: "/route", + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/-bar", - expected: false, + input: "/route.json", + expected: { + path: "/route.json", + params: { test: "route", format: "json" }, + }, }, { - input: "/abc-bar", - expected: false, + input: "/route.json.html", + expected: { + path: "/route.json.html", + params: { test: "route.json", format: "html" }, + }, }, + ], + }, + { + path: "/:test.:format\\z", + tests: [ { - input: "/123-bar", - expected: { path: "/123-bar", params: { test: "123" } }, + input: "/route.htmlz", + expected: { + path: "/route.htmlz", + params: { test: "route", format: "html" }, + }, }, { - input: "/123/456-bar", + input: "/route.html", expected: false, }, ], }, + + /** + * Escaped characters. + */ { - path: "/:test(.*)", + path: "/\\(testing\\)", tests: [ { - input: "/", - expected: { path: "/", params: { test: "" } }, + input: "/testing", + expected: false, }, { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, + input: "/(testing)", + expected: { path: "/(testing)", params: {} }, }, + ], + }, + { + path: "/.\\+\\*\\?\\{\\}=^\\!\\:$\\[\\]\\|", + tests: [ { - input: "/route/123", - expected: { - path: "/route/123", - params: { test: "route/123" }, - }, + input: "/.+*?{}=^!:$[]|", + expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, + ], + }, + + /** + * Random examples. + */ + { + path: "/:foo/:bar", + tests: [ { - input: "/;,:@&=/+$-_.!/~*()", + input: "/match/route", expected: { - path: "/;,:@&=/+$-_.!/~*()", - params: { test: ";,:@&=/+$-_.!/~*()" }, + path: "/match/route", + params: { foo: "match", bar: "route" }, }, }, ], }, { - path: "/:test([a-z]+)", + path: "/:foo\\(test\\)/bar", tests: [ { - input: "/abc", - expected: { path: "/abc", params: { test: "abc" } }, - }, - { - input: "/123", - expected: false, + input: "/foo(test)/bar", + expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { - input: "/abc/123", + input: "/foo/bar", expected: false, }, ], }, { - path: "/:test(this|that)", + path: "/:foo\\?", tests: [ { - input: "/this", - expected: { path: "/this", params: { test: "this" } }, - }, - { - input: "/that", - expected: { path: "/that", params: { test: "that" } }, + input: "/route?", + expected: { path: "/route?", params: { foo: "route" } }, }, { - input: "/foo", + input: "/route", expected: false, }, ], }, { - path: "{/:test(abc|xyz)}*", + path: "/{:pre}baz", tests: [ { - input: "/", - expected: false, + input: "/foobaz", + expected: { path: "/foobaz", params: { pre: "foo" } }, }, { - input: "/abc", - expected: { path: "/abc", params: { test: ["abc"] } }, + input: "/baz", + expected: { path: "/baz", params: { pre: undefined } }, }, + ], + }, + { + path: "/:foo\\(:bar\\)", + tests: [ { - input: "/abc/abc", + input: "/hello(world)", expected: { - path: "/abc/abc", - params: { test: ["abc", "abc"] }, + path: "/hello(world)", + params: { foo: "hello", bar: "world" }, }, }, { - input: "/xyz/xyz", - expected: { - path: "/xyz/xyz", - params: { test: ["xyz", "xyz"] }, - }, + input: "/hello()", + expected: false, }, + ], + }, + { + path: "/:foo\\({:bar}\\)", + tests: [ { - input: "/abc/xyz", + input: "/hello(world)", expected: { - path: "/abc/xyz", - params: { test: ["abc", "xyz"] }, - }, - }, - { - input: "/abc/xyz/abc/xyz", - expected: { - path: "/abc/xyz/abc/xyz", - params: { test: ["abc", "xyz", "abc", "xyz"] }, - }, - }, - { - input: "/xyzxyz", - expected: false, - }, - ], - }, - - /** - * No prefix characters. - */ - { - path: "test", - tests: [ - { - input: "test", - expected: { path: "test", params: {} }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: ":test", - tests: [ - { - input: "route", - expected: { path: "route", params: { test: "route" } }, - }, - { - input: "/route", - expected: false, - }, - { - input: "route/", - expected: false, - }, - ], - }, - { - path: "{:test}?", - tests: [ - { - input: "test", - expected: { path: "test", params: { test: "test" } }, - }, - { - input: "", - expected: { path: "", params: {} }, - }, - ], - }, - { - path: "{:test/}+", - tests: [ - { - input: "route/", - expected: { path: "route/", params: { test: ["route"] } }, - }, - { - input: "/route", - expected: false, - }, - { - input: "", - expected: false, - }, - { - input: "foo/bar/", - expected: { - path: "foo/bar/", - params: { test: ["foo", "bar"] }, - }, - }, - ], - }, - - /** - * Formats. - */ - { - path: "/test.json", - tests: [ - { - input: "/test.json", - expected: { path: "/test.json", params: {} }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: "/:test.json", - tests: [ - { - input: "/.json", - expected: false, - }, - { - input: "/test.json", - expected: { path: "/test.json", params: { test: "test" } }, - }, - { - input: "/route.json", - expected: { path: "/route.json", params: { test: "route" } }, - }, - { - input: "/route.json.json", - expected: false, - }, - ], - }, - { - path: "/:test([^/]+).json", - tests: [ - { - input: "/route.json.json", - expected: { - path: "/route.json.json", - params: { test: "route.json" }, - }, - }, - ], - }, - - /** - * Format params. - */ - { - path: "/test.:format(\\w+)", - tests: [ - { - input: "/test.html", - expected: { path: "/test.html", params: { format: "html" } }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: "/test.:format(\\w+).:format(\\w+)", - tests: [ - { - input: "/test.html.json", - expected: { - path: "/test.html.json", - params: { format: "json" }, - }, - }, - { - input: "/test.html", - expected: false, - }, - ], - }, - { - path: "/test{.:format(\\w+)}?", - tests: [ - { - input: "/test", - expected: { path: "/test", params: { format: undefined } }, - }, - { - input: "/test.html", - expected: { path: "/test.html", params: { format: "html" } }, - }, - ], - }, - { - path: "/test{.:format(\\w+)}+", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test.html", - expected: { - path: "/test.html", - params: { format: ["html"] }, - }, - }, - { - input: "/test.html.json", - expected: { - path: "/test.html.json", - params: { format: ["html", "json"] }, - }, - }, - ], - }, - { - path: "/test{.:format}+", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test.html", - expected: { - path: "/test.html", - params: { format: ["html"] }, - }, - }, - { - input: "/test.hbs.html", - expected: { - path: "/test.hbs.html", - params: { format: ["hbs", "html"] }, - }, - }, - ], - }, - - /** - * Format and path params. - */ - { - path: "/:test.:format", - tests: [ - { - input: "/route.html", - expected: { - path: "/route.html", - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route", - expected: false, - }, - { - input: "/route.html.json", - expected: { - path: "/route.html.json", - params: { test: "route", format: "html.json" }, - }, - }, - ], - }, - { - path: "/:test{.:format}?", - tests: [ - { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, - }, - { - input: "/route.json", - expected: { - path: "/route.json", - params: { test: "route", format: "json" }, - }, - }, - { - input: "/route.json.html", - expected: { - path: "/route.json.html", - params: { test: "route", format: "json.html" }, - }, - }, - ], - }, - { - path: "/:test.:format\\z", - tests: [ - { - input: "/route.htmlz", - expected: { - path: "/route.htmlz", - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route.html", - expected: false, - }, - ], - }, - - /** - * Unnamed params. - */ - { - path: "/(\\d+)", - tests: [ - { - input: "/123", - expected: { path: "/123", params: { "0": "123" } }, - }, - { - input: "/abc", - expected: false, - }, - { - input: "/123/abc", - expected: false, - }, - ], - }, - { - path: "{/(\\d+)}?", - tests: [ - { - input: "/", - expected: false, - }, - { - input: "/123", - expected: { path: "/123", params: { "0": "123" } }, - }, - ], - }, - { - path: "/route\\(\\\\(\\d+\\\\)\\)", - tests: [ - { - input: "/route(\\123\\)", - expected: { - path: "/route(\\123\\)", - params: { "0": "123\\" }, - }, - }, - { - input: "/route(\\123)", - expected: false, - }, - ], - }, - { - path: "{/route}?", - tests: [ - { - input: "", - expected: { path: "", params: {} }, - }, - { - input: "/", - expected: false, - }, - { - input: "/foo", - expected: false, - }, - { - input: "/route", - expected: { path: "/route", params: {} }, - }, - ], - }, - { - path: "{/(.*)}", - tests: [ - { - input: "/", - expected: { path: "/", params: { "0": "" } }, - }, - { - input: "/login", - expected: { path: "/login", params: { "0": "login" } }, - }, - ], - }, - - /** - * Escaped characters. - */ - { - path: "/\\(testing\\)", - tests: [ - { - input: "/testing", - expected: false, - }, - { - input: "/(testing)", - expected: { path: "/(testing)", params: {} }, - }, - ], - }, - { - path: "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", - tests: [ - { - input: "/.+*?{}=^!:$[]|", - expected: { path: "/.+*?{}=^!:$[]|", params: {} }, - }, - ], - }, - { - path: "/test/{:uid(u\\d+)}?{:cid(c\\d+)}?", - tests: [ - { - input: "/test/u123", - expected: { path: "/test/u123", params: { uid: "u123" } }, - }, - { - input: "/test/c123", - expected: { path: "/test/c123", params: { cid: "c123" } }, - }, - ], - }, - - /** - * Unnamed group prefix. - */ - { - path: "/{apple-}?icon-:res(\\d+).png", - tests: [ - { - input: "/icon-240.png", - expected: { path: "/icon-240.png", params: { res: "240" } }, - }, - { - input: "/apple-icon-240.png", - expected: { - path: "/apple-icon-240.png", - params: { res: "240" }, - }, - }, - ], - }, - - /** - * Random examples. - */ - { - path: "/:foo/:bar", - tests: [ - { - input: "/match/route", - expected: { - path: "/match/route", - params: { foo: "match", bar: "route" }, - }, - }, - ], - }, - { - path: "/:foo\\(test\\)/bar", - tests: [ - { - input: "/foo(test)/bar", - expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, - }, - { - input: "/foo/bar", - expected: false, - }, - ], - }, - { - path: "/:remote([\\w\\-\\.]+)/:user([\\w-]+)", - tests: [ - { - input: "/endpoint/user", - expected: { - path: "/endpoint/user", - params: { remote: "endpoint", user: "user" }, - }, - }, - { - input: "/endpoint/user-name", - expected: { - path: "/endpoint/user-name", - params: { remote: "endpoint", user: "user-name" }, - }, - }, - { - input: "/foo.bar/user-name", - expected: { - path: "/foo.bar/user-name", - params: { remote: "foo.bar", user: "user-name" }, - }, - }, - ], - }, - { - path: "/:foo\\?", - tests: [ - { - input: "/route?", - expected: { path: "/route?", params: { foo: "route" } }, - }, - { - input: "/route", - expected: false, - }, - ], - }, - { - path: "{/:foo}+bar", - tests: [ - { - input: "/foobar", - expected: { path: "/foobar", params: { foo: ["foo"] } }, - }, - { - input: "/foo/bar", - expected: false, - }, - { - input: "/foo/barbar", - expected: false, - }, - ], - }, - { - path: "/{:pre}?baz", - tests: [ - { - input: "/foobaz", - expected: { path: "/foobaz", params: { pre: "foo" } }, - }, - { - input: "/baz", - expected: { path: "/baz", params: { pre: undefined } }, - }, - ], - }, - { - path: "/:foo\\(:bar\\)", - tests: [ - { - input: "/hello(world)", - expected: { - path: "/hello(world)", - params: { foo: "hello", bar: "world" }, - }, - }, - { - input: "/hello()", - expected: false, - }, - ], - }, - { - path: "/:foo\\({:bar}?\\)", - tests: [ - { - input: "/hello(world)", - expected: { - path: "/hello(world)", - params: { foo: "hello", bar: "world" }, + path: "/hello(world)", + params: { foo: "hello", bar: "world" }, }, }, { @@ -1377,27 +847,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:postType(video|audio|text){(\\+.+)}?", - tests: [ - { - input: "/video", - expected: { path: "/video", params: { postType: "video" } }, - }, - { - input: "/video+test", - expected: { - path: "/video+test", - params: { 0: "+test", postType: "video" }, - }, - }, - { - input: "/video+", - expected: false, - }, - ], - }, - { - path: "{/:foo}?{/:bar}?-ext", + path: "{/:foo}{/:bar}-ext", tests: [ { input: "/-ext", @@ -1428,7 +878,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:required{/:optional}?-ext", + path: "/:required{/:optional}-ext", tests: [ { input: "/foo-ext", @@ -1545,7 +995,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "mail{.:domain}?.com", + path: "mail{.:domain}.com", options: { delimiter: ".", }, @@ -1608,7 +1058,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Prefixes. */ { - path: "{$:foo}{$:bar}?", + path: "$:foo{$:bar}", tests: [ { input: "$x", @@ -1621,20 +1071,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "{$:foo}+", - tests: [ - { - input: "$x", - expected: { path: "$x", params: { foo: ["x"] } }, - }, - { - input: "$x$y", - expected: { path: "$x$y", params: { foo: ["x", "y"] } }, - }, - ], - }, - { - path: "name{/:attr1}?{-:attr2}?{-:attr3}?", + path: "name{/:attr1}{-:attr2}{-:attr3}", tests: [ { input: "name", @@ -1678,108 +1115,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, ], }, - { - path: "name{/:attrs;-}*", - tests: [ - { - input: "name", - expected: { path: "name", params: {} }, - }, - { - input: "name/1", - expected: { - path: "name/1", - params: { attrs: ["1"] }, - }, - }, - { - input: "name/1-2", - expected: { - path: "name/1-2", - params: { attrs: ["1", "2"] }, - }, - }, - { - input: "name/1-2-3", - expected: { - path: "name/1-2-3", - params: { attrs: ["1", "2", "3"] }, - }, - }, - { - input: "name/foo-bar/route", - expected: false, - }, - { - input: "name/test/route", - expected: false, - }, - ], - }, - - /** - * Nested parentheses. - */ - { - path: "/:test(\\d+(?:\\.\\d+)?)", - tests: [ - { - input: "/123", - expected: { path: "/123", params: { test: "123" } }, - }, - { - input: "/abc", - expected: false, - }, - { - input: "/123/abc", - expected: false, - }, - { - input: "/123.123", - expected: { path: "/123.123", params: { test: "123.123" } }, - }, - { - input: "/123.abc", - expected: false, - }, - ], - }, - { - path: "/:test((?!login)[^/]+)", - tests: [ - { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, - }, - { - input: "/login", - expected: false, - }, - ], - }, /** * https://github.com/pillarjs/path-to-regexp/issues/206 */ { - path: "/user{(s)}?/:user", - tests: [ - { - input: "/user/123", - expected: { path: "/user/123", params: { user: "123" } }, - }, - { - input: "/users/123", - expected: { - path: "/users/123", - params: { 0: "s", user: "123" }, - }, - }, - ], - }, - { - path: "/user{s}?/:user", + path: "/user{s}/:user", tests: [ { input: "/user/123", @@ -1793,250 +1134,176 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, /** - * https://github.com/pillarjs/path-to-regexp/pull/270 - */ - { - path: "/files{/:path}*{.:ext}*", - tests: [ - { - input: "/files/hello/world.txt", - expected: { - path: "/files/hello/world.txt", - params: { path: ["hello", "world"], ext: ["txt"] }, - }, - }, - { - input: "/files/hello/world.txt.png", - expected: { - path: "/files/hello/world.txt.png", - params: { path: ["hello", "world"], ext: ["txt", "png"] }, - }, - }, - { - input: "/files/my/photo.jpg/gif", - expected: false, - }, - ], - }, - { - path: "/files{/:path}*{.:ext}?", - tests: [ - { - input: "/files/hello/world.txt", - expected: { - path: "/files/hello/world.txt", - params: { path: ["hello", "world"], ext: "txt" }, - }, - }, - { - input: "/files/my/photo.jpg/gif", - expected: false, - }, - ], - }, - { - path: "#/*", - tests: [ - { - input: "#/", - expected: { path: "#/", params: {} }, - }, - ], - }, - { - path: "/foo{/:bar}*", - tests: [ - { - input: "/foo/test1/test2", - expected: { - path: "/foo/test1/test2", - params: { bar: ["test1", "test2"] }, - }, - }, - ], - }, - { - path: "/entity/:id/*", - tests: [ - { - input: "/entity/foo", - expected: false, - }, - { - input: "/entity/foo/", - expected: { path: "/entity/foo/", params: { id: "foo" } }, - }, - ], - }, - { - path: "/test/*", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test/", - expected: { path: "/test/", params: {} }, - }, - { - input: "/test/route", - expected: { path: "/test/route", params: { "0": ["route"] } }, - }, - { - input: "/test/route/nested", - expected: { - path: "/test/route/nested", - params: { "0": ["route", "nested"] }, - }, - }, - ], - }, - - /** - * Asterisk wildcard. + * Wildcard. */ { - path: "/*", + path: "/*path", tests: [ { input: "/", - expected: { path: "/", params: { "0": undefined } }, + expected: false, }, { input: "/route", - expected: { path: "/route", params: { "0": ["route"] } }, + expected: { path: "/route", params: { path: ["route"] } }, }, { input: "/route/nested", expected: { path: "/route/nested", - params: { "0": ["route", "nested"] }, + params: { path: ["route", "nested"] }, }, }, ], }, { - path: "*", + path: "*path", tests: [ { input: "/", - expected: { path: "/", params: { "0": ["", ""] } }, + expected: { path: "/", params: { path: ["", ""] } }, }, { input: "/test", - expected: { path: "/test", params: { "0": ["", "test"] } }, + expected: { path: "/test", params: { path: ["", "test"] } }, }, ], }, { - path: "*", + path: "*path", options: { decode: false }, tests: [ { input: "/", - expected: { path: "/", params: { "0": "/" } }, + expected: { path: "/", params: { path: "/" } }, }, { input: "/test", - expected: { path: "/test", params: { "0": "/test" } }, + expected: { path: "/test", params: { path: "/test" } }, }, ], }, { - path: "/*.:ext", + path: "/*path.:ext", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test"], ext: "html" }, + params: { path: ["test"], ext: "html" }, }, }, { input: "/test.html/nested", expected: false, }, + { + input: "/test.html/nested.json", + expected: { + path: "/test.html/nested.json", + params: { path: ["test.html", "nested"], ext: "json" }, + }, + }, ], }, { - path: "/*{.:ext}?", + path: "/:path.*ext", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test.html"], ext: undefined }, + params: { path: "test", ext: ["html"] }, }, }, { input: "/test.html/nested", expected: { - params: { - "0": ["test.html", "nested"], - }, path: "/test.html/nested", + params: { path: "test", ext: ["html", "nested"] }, + }, + }, + { + input: "/test.html/nested.json", + expected: { + path: "/test.html/nested.json", + params: { path: "test", ext: ["html", "nested.json"] }, }, }, ], }, { - path: "/*{.:ext}*", + path: "/*path{.:ext}", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test.html"], ext: undefined }, + params: { path: ["test"], ext: "html" }, }, }, { input: "/test.html/nested", expected: { params: { - "0": ["test.html", "nested"], + path: ["test.html", "nested"], }, path: "/test.html/nested", }, }, ], }, - - /** - * Longer prefix. - */ { - path: "/:foo{/test/:bar}?", + path: "/entity/:id/*path", tests: [ { - input: "/route", - expected: { path: "/route", params: { foo: "route" } }, + input: "/entity/foo", + expected: false, }, { - input: "/route/test/again", + input: "/entity/foo/path", expected: { - path: "/route/test/again", - params: { foo: "route", bar: "again" }, + path: "/entity/foo/path", + params: { id: "foo", path: ["path"] }, + }, + }, + ], + }, + { + path: "/*foo/:bar/*baz", + tests: [ + { + input: "/x/y/z", + expected: { + path: "/x/y/z", + params: { foo: ["x"], bar: "y", baz: ["z"] }, + }, + }, + { + input: "/1/2/3/4/5", + expected: { + path: "/1/2/3/4/5", + params: { foo: ["1", "2", "3"], bar: "4", baz: ["5"] }, }, }, ], }, /** - * Prefix and suffix as separator. + * Longer prefix. */ { - path: "/{<:foo>}+", + path: "/:foo{/test/:bar}", tests: [ { - input: "/", - expected: { path: "/", params: { foo: ["test"] } }, + input: "/route", + expected: { path: "/route", params: { foo: "route" } }, }, { - input: "/", + input: "/route/test/again", expected: { - path: "/", - params: { foo: ["test", "again"] }, + path: "/route/test/again", + params: { foo: "route", bar: "again" }, }, }, ], @@ -2046,7 +1313,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Backtracking tests. */ { - path: "{:foo/}?{:bar.}?", + path: "{:foo/}{:bar.}", tests: [ { input: "", @@ -2066,7 +1333,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/abc{abc:foo}?", + path: "/abc{abc:foo}", tests: [ { input: "/abc", @@ -2094,19 +1361,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:foo{abc:bar}?", + path: "/:foo{abc:bar}", tests: [ { input: "/abc", - expected: false, + expected: { + params: { foo: "abc" }, + path: "/abc", + }, }, { input: "/abcabc", - expected: false, + expected: { + params: { foo: "abcabc" }, + path: "/abcabc", + }, }, { input: "/abcabc123", - expected: false, + expected: { + params: { foo: "abc", bar: "123" }, + path: "/abcabc123", + }, }, { input: "/acb", @@ -2116,10 +1392,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, }, { - input: "/acbabc123", + input: "/123", + expected: { + path: "/123", + params: { foo: "123" }, + }, + }, + { + input: "/123abcabc", expected: { - path: "/acbabc123", - params: { foo: "acb", bar: "123" }, + path: "/123abcabc", + params: { foo: "123abcabc" }, }, }, ], @@ -2137,20 +1420,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabc123", - expected: false, - }, - ], - }, - { - path: "/:foo(.*){.:ext}?", - tests: [ - { - input: "/abc", - expected: { path: "/abc", params: { foo: "abc" } }, + expected: { + path: "/abcabc123", + params: { foo: "abc", bar: "123" }, + }, }, { - input: "/abc.txt", - expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, + input: "/123abcabc", + expected: false, }, ], }, @@ -2186,6 +1463,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + { + path: "/:foo{|:bar|}", + tests: [ + { + input: "/hello|world|", + expected: { + path: "/hello|world|", + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello||", + expected: { path: "/hello||", params: { foo: "hello||" } }, + }, + ], + }, { path: ":foo\\@:bar", tests: [ @@ -2204,7 +1497,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Multi character delimiters. */ { - path: "%25:foo{%25:bar}?", + path: "%25:foo{%25:bar}", options: { delimiter: "%25", }, diff --git a/src/index.bench.ts b/src/index.bench.ts new file mode 100644 index 0000000..9d39219 --- /dev/null +++ b/src/index.bench.ts @@ -0,0 +1,42 @@ +import { bench } from "vitest"; +import { match } from "./index.js"; + +const PATHS: string[] = [ + "/xyz", + "/user", + "/user/123", + "/" + "a".repeat(32_000), + "/-" + "-a".repeat(8_000) + "/-", + "/||||\x00|" + "||".repeat(27387) + "|\x00".repeat(27387) + "/||/", +]; + +const STATIC_PATH_MATCH = match("/user"); +const SIMPLE_PATH_MATCH = match("/user/:id"); +const MULTI_SEGMENT_MATCH = match("/:x/:y"); +const MULTI_PATTERN_MATCH = match("/:x-:y"); +const TRICKY_PATTERN_MATCH = match("/:foo|:bar|"); +const ASTERISK_MATCH = match("/*foo"); + +bench("static path", () => { + for (const path of PATHS) STATIC_PATH_MATCH(path); +}); + +bench("simple path", () => { + for (const path of PATHS) SIMPLE_PATH_MATCH(path); +}); + +bench("multi segment", () => { + for (const path of PATHS) MULTI_SEGMENT_MATCH(path); +}); + +bench("multi pattern", () => { + for (const path of PATHS) MULTI_PATTERN_MATCH(path); +}); + +bench("tricky pattern", () => { + for (const path of PATHS) TRICKY_PATTERN_MATCH(path); +}); + +bench("asterisk", () => { + for (const path of PATHS) ASTERISK_MATCH(path); +}); diff --git a/src/index.spec.ts b/src/index.spec.ts index ef019c9..c6da631 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -6,89 +6,86 @@ import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { - describe("arguments", () => { - it("should throw on non-capturing pattern", () => { - expect(() => match("/:foo(?:\\d+(\\.\\d+)?)")).toThrow( + describe("parse errors", () => { + it("should throw on unbalanced group", () => { + expect(() => parse("/{:foo,")).toThrow( new TypeError( - 'Pattern cannot start with "?" at 6: https://git.new/pathToRegexpError', + "Unexpected END at 7, expected }: https://git.new/pathToRegexpError", ), ); }); - - it("should throw on nested capturing group", () => { - expect(() => match("/:foo(\\d+(\\.\\d+)?)")).toThrow( + it("should throw on nested unbalanced group", () => { + expect(() => parse("/{:foo/{x,y}")).toThrow( new TypeError( - "Capturing groups are not allowed at 9: https://git.new/pathToRegexpError", + "Unexpected END at 12, expected }: https://git.new/pathToRegexpError", ), ); }); - it("should throw on unbalanced pattern", () => { - expect(() => match("/:foo(abc")).toThrow( + it("should throw on missing param name", () => { + expect(() => parse("/:/")).toThrow( new TypeError( - "Unbalanced pattern at 5: https://git.new/pathToRegexpError", + "Missing parameter name at 2: https://git.new/pathToRegexpError", ), ); }); - it("should throw on unmatched )", function () { - expect(() => match("/:fooab)c")).toThrow( - new TypeError("Unmatched ) at 7: https://git.new/pathToRegexpError"), - ); - }); - - it("should throw on unmatched ) after other patterns", function () { - expect(() => match("/:test(\\w+)/:foo(\\d+))")).toThrow( - new TypeError("Unmatched ) at 21: https://git.new/pathToRegexpError"), - ); - }); - - it("should throw on missing pattern", () => { - expect(() => match("/:foo()")).toThrow( + it("should throw on missing wildcard name", () => { + expect(() => parse("/*/")).toThrow( new TypeError( - "Missing pattern at 5: https://git.new/pathToRegexpError", + "Missing parameter name at 2: https://git.new/pathToRegexpError", ), ); }); - it("should throw on missing name", () => { - expect(() => match("/:(test)")).toThrow( + it("should throw on unterminated quote", () => { + expect(() => parse('/:"foo')).toThrow( new TypeError( - "Missing parameter name at 2: https://git.new/pathToRegexpError", + "Unterminated quote at 2: https://git.new/pathToRegexpError", ), ); }); + }); - it("should throw on nested groups", () => { - expect(() => match("/{a{b:foo}}")).toThrow( - new TypeError( - "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", - ), - ); + describe("compile errors", () => { + it("should throw when a param is missing", () => { + const toPath = compile("/a/:b/c"); + + expect(() => { + toPath(); + }).toThrow(new TypeError("Missing parameters: b")); }); - it("should throw on repeat parameters without a separator", () => { - expect(() => match("{:x}*")).toThrow( - new TypeError( - `Missing separator for "x": https://git.new/pathToRegexpError`, - ), - ); + it("should throw when expecting a repeated value", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: [] }); + }).toThrow(new TypeError('Expected "foo" to be a non-empty array')); }); - it("should throw on unterminated quote", () => { - expect(() => match('/:"foo')).toThrow( - new TypeError( - "Unterminated quote at 2: https://git.new/pathToRegexpError", - ), - ); + it("should throw when param gets an array", () => { + const toPath = compile("/:foo"); + + expect(() => { + toPath({ foo: [] }); + }).toThrow(new TypeError('Expected "foo" to be a string')); }); - it("should throw on invalid *", () => { - expect(() => match("/:foo*")).toThrow( - new TypeError( - "Unexpected * at 5, you probably want `/*` or `{/:foo}*`: https://git.new/pathToRegexpError", - ), - ); + it("should throw when a wildcard is not an array", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: "a" }); + }).toThrow(new TypeError('Expected "foo" to be a non-empty array')); + }); + + it("should throw when a wildcard array value is not a string", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: [1, "a"] as any }); + }).toThrow(new TypeError('Expected "foo/0" to be a string')); }); }); @@ -126,75 +123,4 @@ describe("path-to-regexp", () => { }); }, ); - - describe("compile errors", () => { - it("should throw when a required param is undefined", () => { - const toPath = compile("/a/:b/c"); - - expect(() => { - toPath(); - }).toThrow(new TypeError('Expected "b" to be a string')); - }); - - it("should throw when it does not match the pattern", () => { - const toPath = compile("/:foo(\\d+)"); - - expect(() => { - toPath({ foo: "abc" }); - }).toThrow(new TypeError('Invalid value for "foo": "abc"')); - }); - - it("should throw when expecting a repeated value", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: [] }); - }).toThrow(new TypeError('Invalid value for "foo": ""')); - }); - - it("should throw when not expecting a repeated value", () => { - const toPath = compile("/:foo"); - - expect(() => { - toPath({ foo: [] }); - }).toThrow(new TypeError('Expected "foo" to be a string')); - }); - - it("should throw when a repeated param is not an array", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: "a" }); - }).toThrow(new TypeError('Expected "foo" to be an array')); - }); - - it("should throw when an array value is not a string", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: [1, "a"] as any }); - }).toThrow(new TypeError('Expected "foo/0" to be a string')); - }); - - it("should throw when repeated value does not match", () => { - const toPath = compile("{/:foo(\\d+)}+"); - - expect(() => { - toPath({ foo: ["1", "2", "3", "a"] }); - }).toThrow(new TypeError('Invalid value for "foo": "/1/2/3/a"')); - }); - }); }); - -/** - * Execute a regular expression and return a flat array for comparison. - * - * @param {RegExp} re - * @param {String} str - * @return {Array} - */ -function exec(re: RegExp, str: string) { - const match = re.exec(str); - - return match && Array.prototype.slice.call(match); -} diff --git a/src/index.ts b/src/index.ts index 2692df0..a63e365 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,24 +15,13 @@ export type Encode = (value: string) => string; export type Decode = (value: string) => string; export interface ParseOptions { - /** - * The default delimiter for segments. (default: `'/'`) - */ - delimiter?: string; /** * A function for encoding input strings. */ encodePath?: Encode; } -export interface PathOptions { - /** - * Regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; -} - -export interface MatchOptions extends PathOptions { +export interface MatchOptions { /** * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) */ @@ -41,35 +30,47 @@ export interface MatchOptions extends PathOptions { * Matches the path completely without trailing characters. (default: `true`) */ end?: boolean; -} - -export interface CompileOptions extends PathOptions { /** - * Verifies the function is producing a valid path. (default: `true`) + * Allows optional trailing delimiter to match. (default: `true`) + */ + trailing?: boolean; + /** + * Match will be case sensitive. (default: `false`) + */ + sensitive?: boolean; + /** + * The default delimiter for segments. (default: `'/'`) */ - validate?: boolean; + delimiter?: string; +} + +export interface CompileOptions { /** * Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) */ encode?: Encode | false; + /** + * The default delimiter for segments. (default: `'/'`) + */ + delimiter?: string; } type TokenType = | "{" | "}" - | ";" - | "*" - | "+" - | "?" - | "NAME" - | "PATTERN" + | "WILDCARD" + | "PARAM" | "CHAR" | "ESCAPED" | "END" - // Reserved for use. - | "!" - | "@" - | ","; + // Reserved for use or ambiguous due to past use. + | "(" + | ")" + | "[" + | "]" + | "+" + | "?" + | "!"; /** * Tokenizer results. @@ -81,153 +82,120 @@ interface LexToken { } const SIMPLE_TOKENS: Record = { - "!": "!", - "@": "@", - ";": ";", - ",": ",", - "*": "*", - "+": "+", - "?": "?", + // Groups. "{": "{", "}": "}", + // Reserved. + "(": "(", + ")": ")", + "[": "[", + "]": "]", + "+": "+", + "?": "?", + "!": "!", }; +/** + * Escape a regular expression string. + */ +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 ? "s" : "is"; +} + /** * Tokenize input string. */ -function lexer(str: string) { +function* lexer(str: string): Generator { const chars = [...str]; - const tokens: LexToken[] = []; let i = 0; - while (i < chars.length) { - const value = chars[i]; - const type = SIMPLE_TOKENS[value]; - - if (type) { - tokens.push({ type, index: i++, value }); - continue; - } + function name() { + let value = ""; - if (value === "\\") { - tokens.push({ type: "ESCAPED", index: i++, value: chars[i++] }); - continue; - } - - if (value === ":") { - let name = ""; - - if (ID_START.test(chars[++i])) { - name += chars[i]; - while (ID_CONTINUE.test(chars[++i])) { - name += chars[i]; - } - } else if (chars[i] === '"') { - let pos = i; - - while (i < chars.length) { - if (chars[++i] === '"') { - i++; - pos = 0; - break; - } - - if (chars[i] === "\\") { - name += chars[++i]; - } else { - name += chars[i]; - } - } - - if (pos) { - throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); - } - } - - if (!name) { - throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); - } - - tokens.push({ type: "NAME", index: i, value: name }); - continue; - } - - if (value === "(") { - const pos = i++; - let count = 1; - let pattern = ""; - - if (chars[i] === "?") { - throw new TypeError( - `Pattern cannot start with "?" at ${i}: ${DEBUG_URL}`, - ); + if (ID_START.test(chars[++i])) { + value += chars[i]; + while (ID_CONTINUE.test(chars[++i])) { + value += chars[i]; } + } else if (chars[i] === '"') { + let pos = i; while (i < chars.length) { - if (chars[i] === "\\") { - pattern += chars[i++] + chars[i++]; - continue; + if (chars[++i] === '"') { + i++; + pos = 0; + break; } - if (chars[i] === ")") { - count--; - if (count === 0) { - i++; - break; - } - } else if (chars[i] === "(") { - count++; - if (chars[i + 1] !== "?") { - throw new TypeError( - `Capturing groups are not allowed at ${i}: ${DEBUG_URL}`, - ); - } + if (chars[i] === "\\") { + value += chars[++i]; + } else { + value += chars[i]; } - - pattern += chars[i++]; } - if (count) { - throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`); + if (pos) { + throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); } - - if (!pattern) { - throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`); - } - - tokens.push({ type: "PATTERN", index: i, value: pattern }); - continue; } - if (value === ")") { - throw new TypeError(`Unmatched ) at ${i}: ${DEBUG_URL}`); + if (!value) { + throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); } - tokens.push({ type: "CHAR", index: i, value: chars[i++] }); + return value; } - tokens.push({ type: "END", index: i, value: "" }); + while (i < chars.length) { + const value = chars[i]; + const type = SIMPLE_TOKENS[value]; - return new Iter(tokens); + if (type) { + yield { type, index: i++, value }; + } else if (value === "\\") { + yield { type: "ESCAPED", index: i++, value: chars[i++] }; + } else if (value === ":") { + const value = name(); + yield { type: "PARAM", index: i, value }; + } else if (value === "*") { + const value = name(); + yield { type: "WILDCARD", index: i, value }; + } else { + yield { type: "CHAR", index: i, value: chars[i++] }; + } + } + + return { type: "END", index: i, value: "" }; } class Iter { - index = 0; + #peek?: LexToken; - constructor(private tokens: LexToken[]) {} + constructor(private tokens: Generator) {} peek(): LexToken { - return this.tokens[this.index]; + if (!this.#peek) { + const next = this.tokens.next(); + this.#peek = next.value; + } + return this.#peek; } - tryConsume(type: LexToken["type"]): string | undefined { + tryConsume(type: TokenType): string | undefined { const token = this.peek(); if (token.type !== type) return; - this.index++; + this.#peek = undefined; // Reset after consumed. return token.value; } - consume(type: LexToken["type"]): string { + consume(type: TokenType): string { const value = this.tryConsume(type); if (value !== undefined) return value; const { type: nextType, index } = this.peek(); @@ -244,223 +212,216 @@ class Iter { } return result; } +} - modifier(): string | undefined { - return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); - } +/** + * Plain text. + */ +export interface Text { + type: "text"; + value: string; } /** - * Tokenized path instance. Can we passed around instead of string. + * A parameter designed to match arbitrary text within a segment. + */ +export interface Parameter { + type: "param"; + name: string; +} + +/** + * A wildcard parameter designed to match multiple segments. + */ +export interface Wildcard { + type: "wildcard"; + name: string; +} + +/** + * A set of possible tokens to expand when matching. + */ +export interface Group { + type: "group"; + tokens: Token[]; +} + +/** + * A sequence of path match characters. + */ +export type Token = Text | Parameter | Wildcard | Group; + +/** + * Tokenized path instance. */ export class TokenData { - constructor( - public readonly tokens: Token[], - public readonly delimiter: string, - ) {} + constructor(public readonly tokens: Token[]) {} } /** * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): TokenData { - const { encodePath = NOOP_VALUE, delimiter = encodePath(DEFAULT_DELIMITER) } = - options; - const tokens: Token[] = []; - const it = lexer(str); - let key = 0; - - do { - const path = it.text(); - if (path) tokens.push(encodePath(path)); - - const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); - - if (name || pattern) { - tokens.push({ - name: name || String(key++), - pattern, - }); - - const next = it.peek(); - if (next.type === "*") { - throw new TypeError( - `Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: ${DEBUG_URL}`, - ); + const { encodePath = NOOP_VALUE } = options; + const it = new Iter(lexer(str)); + + function consume(endType: TokenType): Token[] { + const tokens: Token[] = []; + + while (true) { + const path = it.text(); + if (path) tokens.push({ type: "text", value: encodePath(path) }); + + const param = it.tryConsume("PARAM"); + if (param) { + tokens.push({ + type: "param", + name: param, + }); + continue; } - continue; - } + const wildcard = it.tryConsume("WILDCARD"); + if (wildcard) { + tokens.push({ + type: "wildcard", + name: wildcard, + }); + continue; + } - const asterisk = it.tryConsume("*"); - if (asterisk) { - tokens.push({ - name: String(key++), - pattern: `${negate(delimiter)}*`, - modifier: "*", - separator: delimiter, - }); - continue; - } + const open = it.tryConsume("{"); + if (open) { + tokens.push({ + type: "group", + tokens: consume("}"), + }); + continue; + } - const open = it.tryConsume("{"); - if (open) { - const prefix = it.text(); - const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); - const suffix = it.text(); - const separator = it.tryConsume(";") && it.text(); - - it.consume("}"); - - const modifier = it.modifier(); - - tokens.push({ - name: name || (pattern ? String(key++) : ""), - prefix: encodePath(prefix), - suffix: encodePath(suffix), - pattern, - modifier, - separator, - }); - continue; + it.consume(endType); + return tokens; } + } - it.consume("END"); - break; - } while (true); + const tokens = consume("END"); + return new TokenData(tokens); +} + +/** + * Transform tokens into a path building function. + */ +function $compile

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

{ + const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = + options; + const fn = tokensToFunction(data.tokens, delimiter, encode); - return new TokenData(tokens, delimiter); + return function path(data: P = {} as P) { + const [path, ...missing] = fn(data); + if (missing.length) { + throw new TypeError(`Missing parameters: ${missing.join(", ")}`); + } + return path; + }; } /** * Compile a string to a template function for the path. */ export function compile

( - path: string, + path: Path, options: CompileOptions & ParseOptions = {}, ) { - return $compile

(parse(path, options), options); + return $compile

( + path instanceof TokenData ? path : 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 === "*"; -} +function tokensToFunction( + tokens: Token[], + delimiter: string, + encode: Encode | false, +) { + const encoders = tokens.map((token) => + tokenToFunction(token, delimiter, encode), + ); -/** - * Check if a key is optional. - */ -export function isOptional(key: Key) { - return key.modifier === "?" || key.modifier === "*"; + return (data: ParamData) => { + const result: string[] = [""]; + + for (const encoder of encoders) { + const [value, ...extras] = encoder(data); + result[0] += value; + result.push(...extras); + } + + return result; + }; } /** * Convert a single token into a path building function. */ -function keyToFunction( - key: Key, +function tokenToFunction( + token: Token, + delimiter: string, encode: Encode | false, -): (data: ParamData) => string { - const encodeValue = encode || NOOP_VALUE; - const { prefix = "", suffix = "", separator = suffix + prefix } = key; - - if (encode && isRepeat(key)) { - const stringify = (value: string, index: number) => { - if (typeof value !== "string") { - throw new TypeError(`Expected "${key.name}/${index}" to be a string`); - } - return encodeValue(value); - }; - - const compile = (value: unknown) => { - if (!Array.isArray(value)) { - throw new TypeError(`Expected "${key.name}" to be an array`); - } +): (data: ParamData) => string[] { + if (token.type === "text") return () => [token.value]; - if (value.length === 0) return ""; + if (token.type === "group") { + const fn = tokensToFunction(token.tokens, delimiter, encode); - return prefix + value.map(stringify).join(separator) + suffix; + return (data) => { + const [value, ...missing] = fn(data); + if (!missing.length) return [value]; + return [""]; }; + } - if (isOptional(key)) { - return (data): string => { - const value = data[key.name]; - if (value == null) return ""; - return value.length ? compile(value) : ""; - }; - } + const encodeValue = encode || NOOP_VALUE; - return (data): string => { - const value = data[key.name]; - return compile(value); - }; - } + if (token.type === "wildcard" && encode !== false) { + return (data) => { + const value = data[token.name]; + if (value == null) return ["", token.name]; - const stringify = (value: unknown) => { - if (typeof value !== "string") { - throw new TypeError(`Expected "${key.name}" to be a string`); - } - return prefix + encodeValue(value) + suffix; - }; + if (!Array.isArray(value) || value.length === 0) { + throw new TypeError(`Expected "${token.name}" to be a non-empty array`); + } - if (isOptional(key)) { - return (data): string => { - const value = data[key.name]; - if (value == null) return ""; - return stringify(value); + return [ + value + .map((value, index) => { + if (typeof value !== "string") { + throw new TypeError( + `Expected "${token.name}/${index}" to be a string`, + ); + } + + return encodeValue(value); + }) + .join(delimiter), + ]; }; } - return (data): string => { - const value = data[key.name]; - return stringify(value); - }; -} + return (data) => { + const value = data[token.name]; + if (value == null) return ["", token.name]; -/** - * Transform tokens into a path building function. - */ -export function $compile

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

{ - const { encode = encodeURIComponent, validate = true } = options; - const flags = toFlags(options); - const sources = toRegExpSource(data, []); - - // Compile all the tokens into regexps. - const encoders: Array<(data: ParamData) => string> = data.tokens.map( - (token, index) => { - if (typeof token === "string") return () => token; - - const fn = keyToFunction(token, encode); - if (!validate) return fn; - - const validRe = new RegExp(`^${sources[index]}$`, flags); - - return (data) => { - const value = fn(data); - if (!validRe.test(value)) { - throw new TypeError( - `Invalid value for "${token.name}": ${JSON.stringify(value)}`, - ); - } - return value; - }; - }, - ); + if (typeof value !== "string") { + throw new TypeError(`Expected "${token.name}" to be a string`); + } - return function path(data: Record = {}) { - let path = ""; - for (const encoder of encoders) path += encoder(data); - return path; + return [encodeValue(value)]; }; } @@ -485,34 +446,38 @@ export type MatchFunction

= (path: string) => Match

; /** * Create path match function from `path-to-regexp` spec. */ -export function $match

( - data: TokenData, +function $match

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

{ - const { decode = decodeURIComponent, end = true } = options; - const { delimiter } = data; - const keys: Key[] = []; + const { + decode = decodeURIComponent, + delimiter = DEFAULT_DELIMITER, + end = true, + trailing = true, + } = options; const flags = toFlags(options); - const sources = toRegExpSource(data, keys); - const re = new RegExp( - `^${sources.join("")}(?=${escape(delimiter)}|$)`, - flags, - ); - - const decoders = keys.map((key) => { - if (!decode) return NOOP_VALUE; + const sources: string[] = []; + const keys: Array = []; - if (isRepeat(key)) { - const { prefix = "", suffix = "", separator = suffix + prefix } = key; - return (value: string) => value.split(separator).map(decode); + for (const { tokens } of data) { + for (const seq of flatten(tokens, 0, [])) { + const regexp = sequenceToRegExp(seq, delimiter, keys); + sources.push(regexp); } + } - return decode; - }); + let pattern = `^(?:${sources.join("|")})`; + if (trailing) pattern += `(?:${escape(delimiter)}$)?`; + pattern += end ? "$" : `(?=${escape(delimiter)}|$)`; - const isValid = end - ? (a: string, b: string) => a.length === b.length - : () => true; + const re = new RegExp(pattern, flags); + + const decoders = keys.map((key) => { + if (decode === false) return NOOP_VALUE; + if (key.type === "param") return decode; + return (value: string) => value.split(delimiter).map(decode); + }); return Object.assign( function match(input: string) { @@ -520,7 +485,6 @@ export function $match

( if (!m) return false; const { 0: path } = m; - if (!isValid(input, path)) return false; const params = Object.create(null); for (let i = 1; i < m.length; i++) { @@ -537,132 +501,97 @@ export function $match

( ); } +export type Path = string | TokenData; + export function match

( - path: string, + path: Path | Path[], options: MatchOptions & ParseOptions = {}, ): MatchFunction

{ - return $match(parse(path, options), options); -} + const paths = Array.isArray(path) ? path : [path]; + const items = paths.map((path) => + path instanceof TokenData ? path : parse(path, options), + ); -/** - * Escape a regular expression string. - */ -function escape(str: string) { - return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); + return $match(items, options); } /** - * Get the flags for a regexp from the options. + * Flattened token set. */ -function toFlags(options: { sensitive?: boolean }) { - return options.sensitive ? "s" : "is"; -} +type Flattened = Text | Parameter | Wildcard; /** - * A key is a capture group in the regex. + * Generate a flat list of sequence tokens from the given tokens. */ -export interface Key { - name: string; - prefix?: string; - suffix?: string; - pattern?: string; - modifier?: string; - separator?: string; -} +function* flatten( + tokens: Token[], + index: number, + init: Flattened[], +): Generator { + if (index === tokens.length) { + return yield init; + } -/** - * A token is a string (nothing special) or key metadata (capture group). - */ -export type Token = string | Key; + const token = tokens[index]; + + if (token.type === "group") { + const fork = init.slice(); + for (const seq of flatten(token.tokens, 0, fork)) { + yield* flatten(tokens, index + 1, seq); + } + } else { + init.push(token); + } + + yield* flatten(tokens, index + 1, init); +} /** - * Convert a token into a regexp string (re-used for path validation). + * Transform a flat sequence of tokens into a regular expression. */ -function toRegExpSource(data: TokenData, keys: Key[]): string[] { - const sources = Array(data.tokens.length); +function sequenceToRegExp( + tokens: Flattened[], + delimiter: string, + keys: Array, +): string { + let result = ""; let backtrack = ""; + let isSafeSegmentParam = true; - let i = data.tokens.length; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; - while (i--) { - const token = data.tokens[i]; - - if (typeof token === "string") { - backtrack = token; - sources[i] = escape(token); + if (token.type === "text") { + result += escape(token.value); + backtrack = token.value; + isSafeSegmentParam ||= token.value.includes(delimiter); continue; } - const { - prefix = "", - suffix = "", - separator = suffix + prefix, - modifier = "", - } = token; - - const pre = escape(prefix); - const post = escape(suffix); - - if (token.name) { - backtrack = suffix || backtrack; - keys.unshift(token); - - if (isRepeat(token)) { - if (!separator) { - throw new TypeError( - `Missing separator for "${token.name}": ${DEBUG_URL}`, - ); - } + if (token.type === "param" || token.type === "wildcard") { + if (!isSafeSegmentParam && !backtrack) { + throw new TypeError(`Missing text after "${token.name}": ${DEBUG_URL}`); + } - const mod = modifier === "*" ? "?" : ""; - const sep = escape(separator); - const pattern = - token.pattern || `${negate(data.delimiter, separator, backtrack)}+`; - - sources[i] = wrap( - pre, - `(?:${pattern})(?:${sep}(?:${pattern}))*`, - post, - mod, - ); + if (token.type === "param") { + result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`; } else { - sources[i] = wrap( - pre, - token.pattern || `${negate(data.delimiter, backtrack)}+`, - post, - modifier, - ); + result += `(.+)`; } - backtrack = prefix; - } else { - sources[i] = `(?:${pre}${post})${modifier}`; - backtrack = `${prefix}${suffix}`; + keys.push(token); + backtrack = ""; + isSafeSegmentParam = false; + continue; } } - return sources; + return result; } -function negate(...args: string[]) { - const values = args.sort().filter((value, index, array) => { - for (let i = 0; i < index; i++) { - const v = array[i]; - if (v.length && value.startsWith(v)) return false; - } - return value.length > 0; - }); - +function negate(delimiter: string, backtrack: string) { + const values = [delimiter, backtrack].filter(Boolean); const isSimple = values.every((value) => value.length === 1); if (isSimple) return `[^${escape(values.join(""))}]`; - return `(?:(?!${values.map(escape).join("|")}).)`; } - -function wrap(pre: string, pattern: string, post: string, modifier: string) { - if (pre || post) { - return `(?:${pre}(${pattern})${post})${modifier}`; - } - - return `(${pattern})${modifier}`; -} diff --git a/tsconfig.build.json b/tsconfig.build.json index d783ab3..3db8e88 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": [] }, - "exclude": ["src/**/*.spec.ts"] + "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"] } From ed1095e0fa78a692e7f3d489e383e7bb1f9d2cc4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 1 Sep 2024 15:14:38 -0700 Subject: [PATCH 49/55] 8.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 172848d..a582d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", diff --git a/package.json b/package.json index a57212a..f6806a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "description": "Express style path to RegExp utility", "keywords": [ "express", From e537daadebe0dfd4b8e869ee7bb0347f444b8a46 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 1 Sep 2024 15:34:01 -0700 Subject: [PATCH 50/55] Add a stringify API --- src/cases.spec.ts | 113 ++++++++++++++++++++++++++++++++++++---------- src/index.spec.ts | 21 +++++++-- src/index.ts | 34 ++++++++++++++ 3 files changed, 142 insertions(+), 26 deletions(-) diff --git a/src/cases.spec.ts b/src/cases.spec.ts index ef06e1f..30bea83 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -1,16 +1,23 @@ -import type { - MatchOptions, - Match, - ParseOptions, - Token, - CompileOptions, - ParamData, +import { + type MatchOptions, + type Match, + type ParseOptions, + type Token, + type CompileOptions, + type ParamData, + TokenData, } from "./index.js"; export interface ParserTestSet { path: string; options?: ParseOptions; - expected: Token[]; + expected: TokenData; +} + +export interface StringifyTestSet { + data: TokenData; + options?: ParseOptions; + expected: string; } export interface CompileTestSet { @@ -34,56 +41,116 @@ export interface MatchTestSet { export const PARSER_TESTS: ParserTestSet[] = [ { path: "/", - expected: [{ type: "text", value: "/" }], + expected: new TokenData([{ type: "text", value: "/" }]), }, { path: "/:test", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "test" }, - ], + ]), }, { path: '/:"0"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "0" }, - ], + ]), }, { path: "/:_", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "_" }, - ], + ]), }, { path: "/:café", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "café" }, - ], + ]), }, { path: '/:"123"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "123" }, - ], + ]), }, { path: '/:"1\\"\\2\\"3"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: '1"2"3' }, - ], + ]), }, { path: "/*path", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "wildcard", name: "path" }, - ], + ]), + }, +]; + +export const STRINGIFY_TESTS: StringifyTestSet[] = [ + { + data: new TokenData([{ type: "text", value: "/" }]), + expected: "/", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + ]), + expected: "/:test", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "café" }, + ]), + expected: "/:café", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "0" }, + ]), + expected: '/:"0"', + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "wildcard", name: "test" }, + ]), + expected: "/*test", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "wildcard", name: "0" }, + ]), + expected: '/*"0"', + }, + { + data: new TokenData([ + { type: "text", value: "/users" }, + { + type: "group", + tokens: [ + { type: "text", value: "/" }, + { type: "param", name: "id" }, + ], + }, + { type: "text", value: "/delete" }, + ]), + expected: "/users{/:id}/delete", + }, + { + data: new TokenData([{ type: "text", value: "/:+?*" }]), + expected: "/\\:\\+\\?\\*", }, ]; diff --git a/src/index.spec.ts b/src/index.spec.ts index c6da631..cef557f 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,6 +1,11 @@ import { describe, it, expect } from "vitest"; -import { parse, compile, match } from "./index.js"; -import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; +import { parse, compile, match, stringify } from "./index.js"; +import { + PARSER_TESTS, + COMPILE_TESTS, + MATCH_TESTS, + STRINGIFY_TESTS, +} from "./cases.spec.js"; /** * Dynamically generate the entire test suite. @@ -94,7 +99,17 @@ describe("path-to-regexp", () => { ({ path, options, expected }) => { it("should parse the path", () => { const data = parse(path, options); - expect(data.tokens).toEqual(expected); + expect(data).toEqual(expected); + }); + }, + ); + + describe.each(STRINGIFY_TESTS)( + "stringify $tokens with $options", + ({ data, expected }) => { + it("should stringify the path", () => { + const path = stringify(data); + expect(path).toEqual(expected); }); }, ); diff --git a/src/index.ts b/src/index.ts index a63e365..8daab82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,13 @@ const SIMPLE_TOKENS: Record = { "!": "!", }; +/** + * Escape text for stringify to path. + */ +function escapeText(str: string) { + return str.replace(/[{}()\[\]+?!:*]/g, "\\$&"); +} + /** * Escape a regular expression string. */ @@ -595,3 +602,30 @@ function negate(delimiter: string, backtrack: string) { if (isSimple) return `[^${escape(values.join(""))}]`; return `(?:(?!${values.map(escape).join("|")}).)`; } + +/** + * Stringify token data into a path string. + */ +export function stringify(data: TokenData) { + return data.tokens.map(stringifyToken).join(""); +} + +function stringifyToken(token: Token): string { + if (token.type === "text") return escapeText(token.value); + if (token.type === "group") { + return `{${token.tokens.map(stringifyToken).join("")}}`; + } + + const isSafe = isNameSafe(token.name); + const key = isSafe ? token.name : JSON.stringify(token.name); + + if (token.type === "param") return `:${key}`; + if (token.type === "wildcard") return `*${key}`; + throw new TypeError(`Unexpected token: ${token}`); +} + +function isNameSafe(name: string) { + const [first, ...rest] = name; + if (!ID_START.test(first)) return false; + return rest.every((char) => ID_CONTINUE.test(char)); +} From c909d1fd8e3cc95992788b918e61db5e2ade9143 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 3 Sep 2024 15:57:44 -0700 Subject: [PATCH 51/55] Stringify names with unsafe text chars after --- src/cases.spec.ts | 16 ++++++++++++++++ src/index.ts | 32 +++++++++++++++++++------------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 30bea83..dee3f29 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -92,6 +92,14 @@ export const PARSER_TESTS: ParserTestSet[] = [ { type: "wildcard", name: "path" }, ]), }, + { + path: '/:"test"stuff', + expected: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + { type: "text", value: "stuff" }, + ]), + }, ]; export const STRINGIFY_TESTS: StringifyTestSet[] = [ @@ -152,6 +160,14 @@ export const STRINGIFY_TESTS: StringifyTestSet[] = [ data: new TokenData([{ type: "text", value: "/:+?*" }]), expected: "/\\:\\+\\?\\*", }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + { type: "text", value: "stuff" }, + ]), + expected: '/:"test"stuff', + }, ]; export const COMPILE_TESTS: CompileTestSet[] = [ diff --git a/src/index.ts b/src/index.ts index 8daab82..2c5a088 100644 --- a/src/index.ts +++ b/src/index.ts @@ -607,21 +607,22 @@ function negate(delimiter: string, backtrack: string) { * Stringify token data into a path string. */ export function stringify(data: TokenData) { - return data.tokens.map(stringifyToken).join(""); -} - -function stringifyToken(token: Token): string { - if (token.type === "text") return escapeText(token.value); - if (token.type === "group") { - return `{${token.tokens.map(stringifyToken).join("")}}`; - } + return data.tokens + .map(function stringifyToken(token, index, tokens): string { + if (token.type === "text") return escapeText(token.value); + if (token.type === "group") { + return `{${token.tokens.map(stringifyToken).join("")}}`; + } - const isSafe = isNameSafe(token.name); - const key = isSafe ? token.name : JSON.stringify(token.name); + const isSafe = + isNameSafe(token.name) && isNextNameSafe(tokens[index + 1]); + const key = isSafe ? token.name : JSON.stringify(token.name); - if (token.type === "param") return `:${key}`; - if (token.type === "wildcard") return `*${key}`; - throw new TypeError(`Unexpected token: ${token}`); + if (token.type === "param") return `:${key}`; + if (token.type === "wildcard") return `*${key}`; + throw new TypeError(`Unexpected token: ${token}`); + }) + .join(""); } function isNameSafe(name: string) { @@ -629,3 +630,8 @@ function isNameSafe(name: string) { if (!ID_START.test(first)) return false; return rest.every((char) => ID_CONTINUE.test(char)); } + +function isNextNameSafe(token: Token | undefined) { + if (token?.type !== "text") return true; + return !ID_CONTINUE.test(token.value[0]); +} From a43e545555bbbb49d08cb7dc1296e0a739c05ea7 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 3 Sep 2024 16:01:43 -0700 Subject: [PATCH 52/55] Move delimiter option to each method --- Readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 263c0f7..902fe90 100644 --- a/Readme.md +++ b/Readme.md @@ -68,6 +68,7 @@ The `match` function returns a function for matching strings against a path: - **options** _(optional)_ (See [parse](#parse) for more options) - **sensitive** Regexp will be case sensitive. (default: `false`) - **end** Validate the match reaches the end of the string. (default: `true`) + - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - **trailing** Allows optional trailing delimiter to match. (default: `true`) - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) @@ -83,6 +84,7 @@ The `compile` function will return a function for transforming parameters into a - **path** A string. - **options** (See [parse](#parse) for more options) + - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) ```js @@ -113,7 +115,6 @@ The `parse` function accepts a string and returns `TokenData`, the set of tokens - **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)) ### Tokens From d6150f5f27e67fcdc532e556a51531e8bdaa498d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Sep 2024 16:59:42 -0700 Subject: [PATCH 53/55] Add pathToRegexp method back --- Readme.md | 25 +++++--- scripts/redos.ts | 10 +-- src/cases.spec.ts | 6 +- src/index.ts | 158 ++++++++++++++++++++++------------------------ 4 files changed, 101 insertions(+), 98 deletions(-) diff --git a/Readme.md b/Readme.md index 902fe90..3c24e66 100644 --- a/Readme.md +++ b/Readme.md @@ -17,11 +17,7 @@ npm install path-to-regexp --save ## Usage ```js -const { match, compile, parse } = require("path-to-regexp"); - -// match(path, options?) -// compile(path, options?) -// parse(path, options?) +const { match, pathToRegexp, compile, parse } = require("path-to-regexp"); ``` ### Parameters @@ -64,20 +60,31 @@ fn("/users/123/delete"); The `match` function returns a function for matching strings against a path: +- **path** String or array of strings. +- **options** _(optional)_ (Extends [pathToRegexp](#pathToRegexp) options) + - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) + +```js +const fn = match("/foo/:bar"); +``` + +**Please note:** `path-to-regexp` is intended for ordered data (e.g. paths, hosts). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). + +## PathToRegexp + +The `pathToRegexp` function returns a regular expression for matching strings against paths. It + - **path** String or array of strings. - **options** _(optional)_ (See [parse](#parse) for more options) - **sensitive** Regexp will be case sensitive. (default: `false`) - **end** Validate the match reaches the end of the string. (default: `true`) - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - **trailing** Allows optional trailing delimiter to match. (default: `true`) - - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) ```js -const fn = match("/foo/:bar"); +const { regexp, keys } = pathToRegexp("/foo/:bar"); ``` -**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). - ## Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: diff --git a/scripts/redos.ts b/scripts/redos.ts index 841cd07..9f0b4bc 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -1,5 +1,5 @@ import { checkSync } from "recheck"; -import { match } from "../src/index.js"; +import { pathToRegexp } from "../src/index.js"; import { MATCH_TESTS } from "../src/cases.spec.js"; let safe = 0; @@ -8,14 +8,14 @@ let fail = 0; const TESTS = MATCH_TESTS.map((x) => x.path); for (const path of TESTS) { - const { re } = match(path) as any; - const result = checkSync(re.source, re.flags); + const { regexp } = pathToRegexp(path); + const result = checkSync(regexp.source, regexp.flags); if (result.status === "safe") { safe++; - console.log("Safe:", path, String(re)); + console.log("Safe:", path, String(regexp)); } else { fail++; - console.log("Fail:", path, String(re)); + console.log("Fail:", path, String(regexp)); } } diff --git a/src/cases.spec.ts b/src/cases.spec.ts index dee3f29..6a7aeec 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -302,6 +302,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ input: "/test/", expected: { path: "/test/", params: {} }, }, + { + input: "/TEST/", + expected: { path: "/TEST/", params: {} }, + }, ], }, { @@ -394,11 +398,11 @@ export const MATCH_TESTS: MatchTestSet[] = [ sensitive: true, }, tests: [ + { input: "/test", expected: false }, { input: "/TEST", expected: { path: "/TEST", params: {} }, }, - { input: "/test", expected: false }, ], }, diff --git a/src/index.ts b/src/index.ts index 2c5a088..5a0d326 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,11 +21,7 @@ export interface ParseOptions { encodePath?: Encode; } -export interface MatchOptions { - /** - * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) - */ - decode?: Decode | false; +export interface PathToRegexpOptions { /** * Matches the path completely without trailing characters. (default: `true`) */ @@ -44,6 +40,13 @@ export interface MatchOptions { delimiter?: string; } +export interface MatchOptions extends PathToRegexpOptions { + /** + * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) + */ + decode?: Decode | false; +} + export interface CompileOptions { /** * Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) @@ -109,13 +112,6 @@ 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 ? "s" : "is"; -} - /** * Tokenize input string. */ @@ -253,6 +249,16 @@ export interface Group { tokens: Token[]; } +/** + * A token that corresponds with a regexp capture. + */ +export type Key = Parameter | Wildcard; + +/** + * A sequence of `path-to-regexp` keys that match capturing groups. + */ +export type Keys = Array; + /** * A sequence of path match characters. */ @@ -316,14 +322,15 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { } /** - * Transform tokens into a path building function. + * Compile a string to a template function for the path. */ -function $compile

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

{ +export function compile

( + path: Path, + options: CompileOptions & ParseOptions = {}, +) { const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = options; + const data = path instanceof TokenData ? path : parse(path, options); const fn = tokensToFunction(data.tokens, delimiter, encode); return function path(data: P = {} as P) { @@ -335,19 +342,6 @@ function $compile

( }; } -/** - * Compile a string to a template function for the path. - */ -export function compile

( - path: Path, - options: CompileOptions & ParseOptions = {}, -) { - return $compile

( - path instanceof TokenData ? path : parse(path, options), - options, - ); -} - export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; @@ -451,34 +445,20 @@ export type Match

= false | MatchResult

; export type MatchFunction

= (path: string) => Match

; /** - * Create path match function from `path-to-regexp` spec. + * Supported path types. */ -function $match

( - data: TokenData[], - options: MatchOptions = {}, -): MatchFunction

{ - const { - decode = decodeURIComponent, - delimiter = DEFAULT_DELIMITER, - end = true, - trailing = true, - } = options; - const flags = toFlags(options); - const sources: string[] = []; - const keys: Array = []; - - for (const { tokens } of data) { - for (const seq of flatten(tokens, 0, [])) { - const regexp = sequenceToRegExp(seq, delimiter, keys); - sources.push(regexp); - } - } - - let pattern = `^(?:${sources.join("|")})`; - if (trailing) pattern += `(?:${escape(delimiter)}$)?`; - pattern += end ? "$" : `(?=${escape(delimiter)}|$)`; +export type Path = string | TokenData; - const re = new RegExp(pattern, flags); +/** + * Transform a path into a match function. + */ +export function match

( + path: Path | Path[], + options: MatchOptions & ParseOptions = {}, +): MatchFunction

{ + const { decode = decodeURIComponent, delimiter = DEFAULT_DELIMITER } = + options; + const { regexp, keys } = pathToRegexp(path, options); const decoders = keys.map((key) => { if (decode === false) return NOOP_VALUE; @@ -486,40 +466,56 @@ function $match

( return (value: string) => value.split(delimiter).map(decode); }); - return Object.assign( - function match(input: string) { - const m = re.exec(input); - if (!m) return false; + return function match(input: string) { + const m = regexp.exec(input); + if (!m) return false; - const { 0: path } = m; - const params = Object.create(null); + const path = m[0]; + const params = Object.create(null); - for (let i = 1; i < m.length; i++) { - if (m[i] === undefined) continue; + for (let i = 1; i < m.length; i++) { + if (m[i] === undefined) continue; - const key = keys[i - 1]; - const decoder = decoders[i - 1]; - params[key.name] = decoder(m[i]); - } + const key = keys[i - 1]; + const decoder = decoders[i - 1]; + params[key.name] = decoder(m[i]); + } - return { path, params }; - }, - { re }, - ); + return { path, params }; + }; } -export type Path = string | TokenData; - -export function match

( +export function pathToRegexp( path: Path | Path[], - options: MatchOptions & ParseOptions = {}, -): MatchFunction

{ + options: PathToRegexpOptions & ParseOptions = {}, +) { + const { + delimiter = DEFAULT_DELIMITER, + end = true, + sensitive = false, + trailing = true, + } = options; + const keys: Keys = []; + const sources: string[] = []; + const flags = sensitive ? "s" : "is"; const paths = Array.isArray(path) ? path : [path]; const items = paths.map((path) => path instanceof TokenData ? path : parse(path, options), ); - return $match(items, options); + for (const { tokens } of items) { + for (const seq of flatten(tokens, 0, [])) { + const regexp = sequenceToRegExp(seq, delimiter, keys); + sources.push(regexp); + } + } + + let pattern = `^(?:${sources.join("|")})`; + if (trailing) pattern += `(?:${escape(delimiter)}$)?`; + pattern += end ? "$" : `(?=${escape(delimiter)}|$)`; + + const regexp = new RegExp(pattern, flags); + return { regexp, keys }; } /** @@ -556,11 +552,7 @@ function* flatten( /** * Transform a flat sequence of tokens into a regular expression. */ -function sequenceToRegExp( - tokens: Flattened[], - delimiter: string, - keys: Array, -): string { +function sequenceToRegExp(tokens: Flattened[], delimiter: string, keys: Keys) { let result = ""; let backtrack = ""; let isSafeSegmentParam = true; From 7b4598c051126cfaccf931abd472fd5e3c14ac95 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Sep 2024 17:08:30 -0700 Subject: [PATCH 54/55] Document stringify method --- Readme.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 3c24e66..455c935 100644 --- a/Readme.md +++ b/Readme.md @@ -17,7 +17,13 @@ npm install path-to-regexp --save ## Usage ```js -const { match, pathToRegexp, compile, parse } = require("path-to-regexp"); +const { + match, + pathToRegexp, + compile, + parse, + stringify, +} = require("path-to-regexp"); ``` ### Parameters @@ -111,6 +117,21 @@ const toPathRaw = compile("/user/:id", { encode: false }); toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" ``` +## Stringify + +Transform `TokenData` (a sequence of tokens) back into a Path-to-RegExp string. + +- **data** A `TokenData` instance + +```js +const data = new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "foo" }, +]); + +const path = stringify(data); //=> "/:foo" +``` + ## Developers - If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around. From c302644003b09c3a3a09ba645f44dad6eaf131d5 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Sep 2024 17:22:42 -0700 Subject: [PATCH 55/55] 8.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a582d4b..ab36824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "path-to-regexp", - "version": "8.0.0", + "version": "8.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "path-to-regexp", - "version": "8.0.0", + "version": "8.1.0", "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", diff --git a/package.json b/package.json index f6806a5..b9a6d43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "path-to-regexp", - "version": "8.0.0", + "version": "8.1.0", "description": "Express style path to RegExp utility", "keywords": [ "express",