pax_global_header00006660000000000000000000000064151612764500014521gustar00rootroot0000000000000052 comment=34cb451ddaeea4783a2fe60579ffb3e4ccfc73a7 pillarjs-path-to-regexp-34cdb0e/000077500000000000000000000000001516127645000166715ustar00rootroot00000000000000pillarjs-path-to-regexp-34cdb0e/.editorconfig000066400000000000000000000002741516127645000213510ustar00rootroot00000000000000# EditorConfig: http://EditorConfig.org root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 2 pillarjs-path-to-regexp-34cdb0e/.github/000077500000000000000000000000001516127645000202315ustar00rootroot00000000000000pillarjs-path-to-regexp-34cdb0e/.github/workflows/000077500000000000000000000000001516127645000222665ustar00rootroot00000000000000pillarjs-path-to-regexp-34cdb0e/.github/workflows/ci.yml000066400000000000000000000010301516127645000233760ustar00rootroot00000000000000name: CI on: - push - pull_request permissions: contents: read jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: matrix: node-version: - "18" - "*" steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test - uses: codecov/codecov-action@v5 with: name: Node.js ${{ matrix.node-version }} pillarjs-path-to-regexp-34cdb0e/.github/workflows/codeql.yml000066400000000000000000000047511516127645000242670ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: ["master"] pull_request: # The branches below must be a subset of the branches above branches: ["master"] schedule: - cron: "0 0 * * 1" permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["typescript"] # CodeQL supports [ $supported-codeql-languages ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" pillarjs-path-to-regexp-34cdb0e/.github/workflows/scorecard.yml000066400000000000000000000056541516127645000247700ustar00rootroot00000000000000# 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: contents: read 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@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # 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 pillarjs-path-to-regexp-34cdb0e/.gitignore000066400000000000000000000001241516127645000206560ustar00rootroot00000000000000.vscode/ node_modules/ coverage/ dist/ dist.es2015/ *.tsbuildinfo package-lock.json pillarjs-path-to-regexp-34cdb0e/History.md000066400000000000000000000115021516127645000206530ustar00rootroot00000000000000# Moved to [GitHub Releases](https://github.com/pillarjs/path-to-regexp/releases) ## 3.0.0 / 2019-01-13 - Always use prefix character as delimiter token, allowing any character to be a delimiter (e.g. `/:att1-:att2-:att3-:att4-:att5`) - Remove `partial` support, prefer escaping the prefix delimiter explicitly (e.g. `\\/(apple-)?icon-:res(\\d+).png`) ## 2.4.0 / 2018-08-26 - Support `start` option to disable anchoring from beginning of the string ## 2.3.0 / 2018-08-20 - Use `delimiter` when processing repeated matching groups (e.g. `foo/bar` has no prefix, but has a delimiter) ## 2.2.1 / 2018-04-24 - Allow empty string with `end: false` to match both relative and absolute paths ## 2.2.0 / 2018-03-06 - Pass `token` as second argument to `encode` option (e.g. `encode(value, token)`) ## 2.1.0 / 2017-10-20 - Handle non-ending paths where the final character is a delimiter - E.g. `/foo/` before required either `/foo/` or `/foo//` to match in non-ending mode ## 2.0.0 / 2017-08-23 - New option! Ability to set `endsWith` to match paths like `/test?query=string` up to the query string - New option! Set `delimiters` for specific characters to be treated as parameter prefixes (e.g. `/:test`) - Remove `isarray` dependency - Explicitly handle trailing delimiters instead of trimming them (e.g. `/test/` is now treated as `/test/` instead of `/test` when matching) - Remove overloaded `keys` argument that accepted `options` - Remove `keys` list attached to the `RegExp` output - Remove asterisk functionality (it's a real pain to properly encode) - Change `tokensToFunction` (e.g. `compile`) to accept an `encode` function for pretty encoding (e.g. pass your own implementation) ## 1.7.0 / 2016-11-08 - Allow a `delimiter` option to be passed in with `tokensToRegExp` which will be used for "non-ending" token match situations ## 1.6.0 / 2016-10-03 - Populate `RegExp.keys` when using the `tokensToRegExp` method (making it consistent with the main export) - Allow a `delimiter` option to be passed in with `parse` - Updated TypeScript definition with `Keys` and `Options` updated ## 1.5.3 / 2016-06-15 - Add `\\` to the ignore character group to avoid backtracking on mismatched parens ## 1.5.2 / 2016-06-15 - Escape `\\` in string segments of regexp ## 1.5.1 / 2016-06-08 - Add `index.d.ts` to NPM package ## 1.5.0 / 2016-05-20 - Handle partial token segments (better) - Allow compile to handle asterisk token segments ## 1.4.0 / 2016-05-18 - Handle RegExp unions in path matching groups ## 1.3.0 / 2016-05-08 - Clarify README language and named parameter token support - Support advanced Closure Compiler with type annotations - Add pretty paths options to compiled function output - Add TypeScript definition to project - Improved prefix handling with non-complete segment parameters (E.g. `/:foo?-bar`) ## 1.2.1 / 2015-08-17 - Encode values before validation with path compilation function - More examples of using compilation in README ## 1.2.0 / 2015-05-20 - Add support for matching an asterisk (`*`) as an unnamed match everything group (`(.*)`) ## 1.1.1 / 2015-05-11 - Expose methods for working with path tokens ## 1.1.0 / 2015-05-09 - Expose the parser implementation to consumers - Implement a compiler function to generate valid strings - Huge refactor of tests to be more DRY and cover new parse and compile functions - Use chai in tests - Add .editorconfig ## 1.0.3 / 2015-01-17 - Optimised function runtime - Added `files` to `package.json` ## 1.0.2 / 2014-12-17 - Use `Array.isArray` shim - Remove ES5 incompatible code - Fixed repository path - Added new readme badges ## 1.0.1 / 2014-08-27 - Ensure installation works correctly on 0.8 ## 1.0.0 / 2014-08-17 - No more API changes ## 0.2.5 / 2014-08-07 - Allow keys parameter to be omitted ## 0.2.4 / 2014-08-02 - Code coverage badge - Updated readme - Attach keys to the generated regexp ## 0.2.3 / 2014-07-09 - Add MIT license ## 0.2.2 / 2014-07-06 - A passed in trailing slash in non-strict mode will become optional - In non-end mode, the optional trailing slash will only match at the end ## 0.2.1 / 2014-06-11 - Fixed a major capturing group regexp regression ## 0.2.0 / 2014-06-09 - Improved support for arrays - Improved support for regexps - Better support for non-ending strict mode matches with a trailing slash - Travis CI support - Block using regexp special characters in the path - Removed support for the asterisk to match all - New support for parameter suffixes - `*`, `+` and `?` - Updated readme - Provide delimiter information with keys array ## 0.1.2 / 2014-03-10 - Move testing dependencies to `devDependencies` ## 0.1.1 / 2014-03-10 - Match entire substring with `options.end` - Properly handle ending and non-ending matches ## 0.1.0 / 2014-03-06 - Add `options.end` ## 0.0.2 / 2013-02-10 - Update to match current express - Add .license property to component.json pillarjs-path-to-regexp-34cdb0e/LICENSE000066400000000000000000000021171516127645000176770ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pillarjs-path-to-regexp-34cdb0e/Readme.md000066400000000000000000000172521516127645000204170ustar00rootroot00000000000000# Path-to-RegExp > Turn a path string such as `/user/:name` into a regular expression. [![NPM version][npm-image]][npm-url] [![NPM downloads][downloads-image]][downloads-url] [![Build status][build-image]][build-url] [![Build coverage][coverage-image]][coverage-url] [![License][license-image]][license-url] ## Installation ``` npm install path-to-regexp --save ``` ## Usage ```js const { match, pathToRegexp, compile, parse, stringify, } = require("path-to-regexp"); ``` ### Parameters 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"); fn("/test/route"); //=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` ### Wildcard 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("/*splat"); fn("/bar/baz"); //=> { path: '/bar/baz', params: { splat: [ 'bar', 'baz' ] } } ``` ### Optional Braces can be used to define parts of the path that are optional. ```js const fn = match("/users{/:id}/delete"); fn("/users/delete"); //=> { path: '/users/delete', params: {} } fn("/users/123/delete"); //=> { path: '/users/123/delete', params: { id: '123' } } ``` ## Match The `match` function returns a function for matching strings against a path: - **path** String, `TokenData` object, or array of strings and `TokenData` objects. - **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 the `regexp` for matching strings against paths, and an array of `keys` for understanding the `RegExp#exec` matches. - **path** String, `TokenData` object, or array of strings and `TokenData` objects. - **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`) ```js const { regexp, keys } = pathToRegexp("/foo/:bar"); regexp.exec("/foo/123"); //=> ["/foo/123", "123"] ``` ## Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: - **path** A string or `TokenData` object. - **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 const toPath = compile("/user/:id"); toPath({ id: "name" }); //=> "/user/name" toPath({ id: "café" }); //=> "/user/caf%C3%A9" const toPathRepeated = compile("/*segment"); toPathRepeated({ segment: ["foo"] }); //=> "/foo" toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c" // 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" ``` ## Stringify Transform a `TokenData` object to a Path-to-RegExp string. - **data** A `TokenData` object. ```js const data = { tokens: [ { 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. - 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`, which can be used with `match` and `compile`. - **path** A string. - **options** _(optional)_ - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) ### Tokens `TokenData` has two properties: - **tokens** A sequence of tokens, currently of types `text`, `parameter`, `wildcard`, or `group`. - **originalPath** The original path used with `parse`, shown in error messages to assist debugging. ### Custom path In some applications you may not be able to use the `path-to-regexp` syntax, but you still want to use this library for `match` and `compile`. For example: ```js import { match } from "path-to-regexp"; const tokens = [ { type: "text", value: "/" }, { type: "parameter", name: "foo" }, ]; const originalPath = "/[foo]"; // To help debug error messages. const path = { tokens, originalPath }; 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. ### Missing parameter name Parameter names must be provided after `:` or `*`, for example `/*path`. They can be valid JavaScript identifiers (e.g. `:myName`) or JSON strings (`:"my-name"`). ### Unexpected `?` or `+` In past releases, `?`, `*`, and `+` were used to denote optional or repeating parameters. As an alternative, try these: - For optional (`?`), use braces: `/file{.:ext}`. - For one or more (`+`), use a wildcard: `/*path`. - For zero or more (`*`), use both: `/files{/*path}`. ### Unexpected `(`, `)`, `[`, `]`, etc. 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 match these characters literally, escape them with a backslash, e.g. `"\\("`. ### Unterminated quote Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character. For example, `:"foo`. ### Express <= 4.x Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways: - The wildcard `*` must have a name and matches the behavior of parameters `:`. - The optional character `?` is no longer supported, use braces instead: `/:file{.:ext}`. - Regexp characters are not supported. - Some characters have been reserved to avoid confusion during upgrade (`()[]?+!`). - Parameter names now support valid JavaScript identifiers, or quoted like `:"this"`. ## License MIT [npm-image]: https://img.shields.io/npm/v/path-to-regexp [npm-url]: https://npmjs.org/package/path-to-regexp [downloads-image]: https://img.shields.io/npm/dm/path-to-regexp [downloads-url]: https://npmjs.org/package/path-to-regexp [build-image]: https://img.shields.io/github/actions/workflow/status/pillarjs/path-to-regexp/ci.yml?branch=master [build-url]: https://github.com/pillarjs/path-to-regexp/actions/workflows/ci.yml?query=branch%3Amaster [coverage-image]: https://img.shields.io/codecov/c/gh/pillarjs/path-to-regexp [coverage-url]: https://codecov.io/gh/pillarjs/path-to-regexp [license-image]: http://img.shields.io/npm/l/path-to-regexp.svg?style=flat [license-url]: LICENSE.md pillarjs-path-to-regexp-34cdb0e/package.json000066400000000000000000000025721516127645000211650ustar00rootroot00000000000000{ "name": "path-to-regexp", "version": "8.4.0", "description": "Express style path to RegExp utility", "keywords": [ "express", "regexp", "route", "routing" ], "repository": { "type": "git", "url": "https://github.com/pillarjs/path-to-regexp.git" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" }, "license": "MIT", "exports": "./dist/index.js", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ "dist/" ], "scripts": { "bench": "vitest bench", "build": "ts-scripts build", "format": "ts-scripts format", "lint": "ts-scripts lint", "prepare": "ts-scripts install && npm run build", "size": "size-limit", "specs": "ts-scripts specs", "test": "ts-scripts test && npm run size" }, "devDependencies": { "@borderless/ts-scripts": "^0.15.0", "@size-limit/preset-small-lib": "^11.1.2", "@types/node": "^22.7.2", "@types/semver": "^7.3.1", "@vitest/coverage-v8": "^3.0.5", "recheck": "^4.5.0", "size-limit": "^11.1.2", "typescript": "^5.7.3", "vitest": "^3.0.5" }, "publishConfig": { "access": "public" }, "size-limit": [ { "path": "dist/index.js", "limit": "2.15 kB" } ], "ts-scripts": { "dist": [ "dist" ], "project": [ "tsconfig.build.json" ] } } pillarjs-path-to-regexp-34cdb0e/src/000077500000000000000000000000001516127645000174605ustar00rootroot00000000000000pillarjs-path-to-regexp-34cdb0e/src/cases.spec.ts000066400000000000000000001243431516127645000220660ustar00rootroot00000000000000import { type MatchOptions, type Match, type ParseOptions, type Token, type CompileOptions, type ParamData, TokenData, Path, } from "./index.js"; export interface ParserTestSet { path: string; options?: ParseOptions; expected: TokenData; } export interface StringifyTestSet { data: TokenData; options?: ParseOptions; expected: string; } export interface CompileTestSet { path: Path; options?: CompileOptions & ParseOptions; tests: Array<{ input: ParamData | undefined; expected: string | null; }>; } export interface MatchTestSet { path: Path | Path[]; options?: MatchOptions & ParseOptions; tests: Array<{ input: string; expected: Match; }>; } export const PARSER_TESTS: ParserTestSet[] = [ { path: "/", expected: new TokenData([{ type: "text", value: "/" }], "/"), }, { path: "/:test", expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "test" }, ], "/:test", ), }, { path: "/:a:b", expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "a" }, { type: "param", name: "b" }, ], "/:a:b", ), }, { path: '/:"0"', expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "0" }, ], '/:"0"', ), }, { path: "/:_", expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "_" }, ], "/:_", ), }, { path: "/:café", expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "café" }, ], "/:café", ), }, { path: '/:"123"', expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "123" }, ], '/:"123"', ), }, { path: '/:"1\\"\\2\\"3"', expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: '1"2"3' }, ], '/:"1\\"\\2\\"3"', ), }, { path: "/*path", expected: new TokenData( [ { type: "text", value: "/" }, { type: "wildcard", name: "path" }, ], "/*path", ), }, { path: '/:"test"stuff', expected: new TokenData( [ { type: "text", value: "/" }, { type: "param", name: "test" }, { type: "text", value: "stuff" }, ], '/:"test"stuff', ), }, { path: "\\\\:test", expected: new TokenData( [ { type: "text", value: "\\" }, { type: "param", name: "test" }, ], "\\\\:test", ), }, ]; 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: "/\\:\\+\\?\\*", }, { data: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "test" }, { type: "text", value: "stuff" }, ]), expected: '/:"test"stuff', }, { data: new TokenData([ { type: "text", value: "\\" }, { type: "param", name: "test" }, ]), expected: "\\\\:test", }, { data: { tokens: [ { type: "text", value: "/" }, { type: "param", name: "test" }, ], originalPath: "/:test", }, expected: "/:test", }, ]; 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", tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, { input: { test: "123" }, expected: "/123" }, { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, ], }, { path: "/:test", options: { 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: () => "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: "/123/xyz" }, ], }, { path: "/*test", tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, { input: { test: [] }, expected: null }, { input: { test: ["123"] }, expected: "/123" }, { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, ], }, { path: "/*test", options: { encode: false }, tests: [ { input: { test: "123" }, expected: "/123" }, { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, { path: { tokens: [ { type: "text", value: "/" }, { type: "param", name: "test" }, ], }, tests: [{ input: { test: "123" }, expected: "/123" }], }, ]; /** * An array of test cases with expected inputs and outputs. */ export const MATCH_TESTS: MatchTestSet[] = [ /** * Simple paths. */ { path: "/", tests: [ { input: "/", expected: { path: "/", params: {} }, }, { input: "/route", expected: false }, ], }, { path: "/test", tests: [ { input: "/test", expected: { path: "/test", params: {} }, }, { input: "/route", expected: false }, { input: "/test/route", expected: false }, { input: "/test/", expected: { path: "/test/", params: {} }, }, { input: "/TEST/", expected: { path: "/TEST/", params: {} }, }, ], }, { path: "/test/", tests: [ { input: "/test/", expected: { path: "/test/", params: {} }, }, { input: "/route", expected: false }, { input: "/test", expected: false }, { input: "/test//", expected: { path: "/test//", params: {} }, }, ], }, { path: "/:test", tests: [ { input: "/route", expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", expected: { path: "/route.json", params: { test: "route.json" }, }, }, { input: "/route.json/", expected: { path: "/route.json/", params: { test: "route.json" }, }, }, { input: "/route/test", expected: false, }, { input: "/caf%C3%A9", expected: { path: "/caf%C3%A9", params: { test: "café" }, }, }, { input: "/;,:@&=+$-_.!~*()", expected: { path: "/;,:@&=+$-_.!~*()", params: { test: ";,:@&=+$-_.!~*()" }, }, }, { input: "/param%2523", expected: { path: "/param%2523", params: { test: "param%23" }, }, }, ], }, /** * Case-sensitive paths. */ { path: "/test", options: { sensitive: true, }, tests: [ { input: "/test", expected: { path: "/test", params: {} }, }, { input: "/TEST", expected: false }, ], }, { path: "/TEST", options: { sensitive: true, }, tests: [ { input: "/test", expected: false }, { input: "/TEST", expected: { path: "/TEST", params: {} }, }, ], }, /** * Non-ending mode. */ { path: "/test", options: { end: false, }, tests: [ { input: "/test", expected: { path: "/test", params: {} }, }, { input: "/test/", expected: { path: "/test/", params: {} }, }, { input: "/test////", expected: { path: "/test", params: {} }, }, { input: "/route/test", expected: false, }, { input: "/test/route", expected: { path: "/test", params: {} }, }, { input: "/route", expected: false, }, ], }, { path: "/test/", options: { end: false, }, tests: [ { input: "/test", expected: false, }, { input: "/test/", expected: { path: "/test/", params: {} }, }, { input: "/test//", expected: { path: "/test//", params: {} }, }, { input: "/test/route", expected: false, }, { input: "/route/test/deep", expected: false, }, ], }, { path: "/:test", options: { end: false, }, tests: [ { input: "/route", expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", expected: { path: "/route.json", params: { test: "route.json" }, }, }, { input: "/route.json/", expected: { path: "/route.json/", params: { test: "route.json" }, }, }, { input: "/route/test", expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json/test", expected: { path: "/route.json", params: { test: "route.json" }, }, }, { input: "/caf%C3%A9", expected: { path: "/caf%C3%A9", params: { test: "café" }, }, }, ], }, { path: "/:test/", options: { end: false, }, tests: [ { input: "/route", expected: false, }, { input: "/route/", expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route/test", expected: false, }, { input: "/route/test/", expected: false, }, { input: "/route//test", expected: { path: "/route/", params: { test: "route" } }, }, ], }, { path: "", options: { end: false, }, tests: [ { input: "", expected: { path: "", params: {} }, }, { input: "/", expected: { path: "/", params: {} }, }, { input: "route", expected: false, }, { input: "/route", expected: { path: "", params: {} }, }, { input: "/route/", expected: { path: "", params: {} }, }, ], }, /** * Optional. */ { 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", expected: { path: "/route", params: { test: "route" } }, }, { input: "", expected: { path: "", params: {} }, }, { input: "/", expected: { path: "/", params: {} }, }, ], }, { path: "{/:test}/bar", tests: [ { input: "/bar", expected: { path: "/bar", params: {} }, }, { input: "/foo/bar", expected: { path: "/foo/bar", params: { test: "foo" } }, }, { input: "/foo/bar/", expected: { path: "/foo/bar/", params: { test: "foo" } }, }, ], }, { path: "{/:test}-bar", tests: [ { input: "-bar", expected: { path: "-bar", params: {} }, }, { input: "/foo-bar", expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, { path: "/{:test}-bar", tests: [ { input: "/-bar", expected: { path: "/-bar", params: {} }, }, { input: "/foo-bar", expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, /** * 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: { path: "route/", params: { test: "route" } }, }, ], }, { path: "{:test}", tests: [ { input: "test", expected: { path: "test", params: { test: "test" } }, }, { input: "", expected: { path: "", params: {} }, }, ], }, /** * 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: { path: "/route.json.json", params: { test: "route.json" } }, }, ], }, /** * 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.html", format: "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.json", format: "html" }, }, }, ], }, { path: "/:test.:format\\z", tests: [ { input: "/route.htmlz", expected: { path: "/route.htmlz", params: { test: "route", format: "html" }, }, }, { input: "/route.html", expected: false, }, ], }, /** * Escaped characters. */ { path: "/\\(testing\\)", tests: [ { input: "/testing", expected: false, }, { input: "/(testing)", expected: { path: "/(testing)", params: {} }, }, ], }, { path: "/.\\+\\*\\?\\{\\}=^\\!\\:$\\[\\]\\|", tests: [ { input: "/.+*?{}=^!:$[]|", expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, ], }, /** * 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: "/:foo\\?", tests: [ { input: "/route?", expected: { path: "/route?", params: { foo: "route" } }, }, { input: "/route", 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" }, }, }, { input: "/hello()", expected: { path: "/hello()", params: { foo: "hello", bar: undefined }, }, }, ], }, { path: "{/:foo}{/:bar}-ext", tests: [ { input: "/-ext", expected: false, }, { input: "-ext", expected: { path: "-ext", params: { foo: undefined, bar: undefined }, }, }, { input: "/foo-ext", expected: { path: "/foo-ext", params: { foo: "foo" } }, }, { input: "/foo/bar-ext", expected: { path: "/foo/bar-ext", params: { foo: "foo", bar: "bar" }, }, }, { input: "/foo/-ext", expected: false, }, ], }, { path: "/:required{/:optional}-ext", tests: [ { input: "/foo-ext", expected: { path: "/foo-ext", params: { required: "foo" } }, }, { input: "/foo/bar-ext", expected: { path: "/foo/bar-ext", params: { required: "foo", optional: "bar" }, }, }, { input: "/foo/-ext", expected: false, }, ], }, /** * Unicode matches. */ { path: "/:foo", tests: [ { input: "/café", expected: { path: "/café", params: { foo: "café" } }, }, ], }, { path: "/:foo", options: { decode: false, }, tests: [ { input: "/caf%C3%A9", expected: { path: "/caf%C3%A9", params: { foo: "caf%C3%A9" }, }, }, ], }, { path: "/café", tests: [ { input: "/café", expected: { path: "/café", params: {} }, }, ], }, { path: "/café", options: { encodePath: encodeURI, }, tests: [ { input: "/caf%C3%A9", expected: { path: "/caf%C3%A9", params: {} }, }, ], }, /** * Hostnames. */ { path: ":domain.com", options: { delimiter: ".", }, tests: [ { input: "example.com", expected: { path: "example.com", params: { domain: "example" }, }, }, { input: "github.com", expected: { path: "github.com", params: { domain: "github" }, }, }, ], }, { path: "mail.:domain.com", options: { delimiter: ".", }, tests: [ { input: "mail.example.com", expected: { path: "mail.example.com", params: { domain: "example" }, }, }, { input: "mail.github.com", expected: { path: "mail.github.com", params: { domain: "github" }, }, }, ], }, { path: "mail{.:domain}.com", options: { delimiter: ".", }, tests: [ { input: "mail.com", expected: { path: "mail.com", params: { domain: undefined } }, }, { input: "mail.example.com", expected: { path: "mail.example.com", params: { domain: "example" }, }, }, { input: "mail.github.com", expected: { path: "mail.github.com", params: { domain: "github" }, }, }, ], }, { path: "example.:ext", options: { delimiter: ".", }, tests: [ { input: "example.com", expected: { path: "example.com", params: { ext: "com" } }, }, { input: "example.org", expected: { path: "example.org", params: { ext: "org" } }, }, ], }, { path: "this is", options: { delimiter: " ", end: false, }, tests: [ { input: "this is a test", expected: { path: "this is", params: {} }, }, { input: "this isn't", expected: false, }, ], }, /** * Prefixes. */ { path: "$:foo{$:bar}", tests: [ { input: "$x", expected: { path: "$x", params: { foo: "x" } }, }, { input: "$x$y", expected: { path: "$x$y", params: { foo: "x", bar: "y" } }, }, ], }, { path: "name{/:attr1}{-:attr2}{-:attr3}", tests: [ { input: "name", expected: { path: "name", params: {} }, }, { input: "name/test", expected: { path: "name/test", params: { attr1: "test" }, }, }, { input: "name/1", expected: { path: "name/1", params: { attr1: "1" }, }, }, { input: "name/1-2", expected: { path: "name/1-2", params: { attr1: "1", attr2: "2" }, }, }, { input: "name/1-2-3", expected: { path: "name/1-2-3", params: { attr1: "1", attr2: "2", attr3: "3" }, }, }, { input: "name/foo-bar/route", expected: false, }, { input: "name/test/route", 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: { user: "123" } }, }, ], }, /** * Wildcard. */ { path: "/*path", tests: [ { input: "/", expected: false, }, { input: "/route", expected: { path: "/route", params: { path: ["route"] } }, }, { input: "/route/nested", expected: { path: "/route/nested", params: { path: ["route", "nested"] }, }, }, ], }, { path: "*path", tests: [ { input: "/", expected: { path: "/", params: { path: ["", ""] } }, }, { input: "/test", expected: { path: "/test", params: { path: ["", "test"] } }, }, ], }, { path: "*path", options: { decode: false }, tests: [ { input: "/", expected: { path: "/", params: { path: "/" } }, }, { input: "/test", expected: { path: "/test", params: { path: "/test" } }, }, ], }, { path: "/*path.:ext", tests: [ { input: "/test.html", expected: { path: "/test.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: "/:path.*ext", tests: [ { input: "/test.html", expected: { path: "/test.html", params: { path: "test", ext: ["html"] }, }, }, { input: "/test.html/nested", expected: { 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: "/*path{.:ext}", tests: [ { input: "/test.html", expected: { path: "/test.html", params: { path: ["test"], ext: "html" }, }, }, { input: "/test.html/nested", expected: { params: { path: ["test.html", "nested"], }, path: "/test.html/nested", }, }, ], }, { path: "/entity/:id/*path", tests: [ { input: "/entity/foo", expected: false, }, { input: "/entity/foo/path", expected: { 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"] }, }, }, ], }, /** * Longer prefix. */ { path: "/:foo{/test/:bar}", tests: [ { input: "/route", expected: { path: "/route", params: { foo: "route" } }, }, { input: "/route/test/again", expected: { path: "/route/test/again", params: { foo: "route", bar: "again" }, }, }, ], }, /** * Backtracking tests. */ { path: "{:foo/}{:bar.}", tests: [ { input: "", expected: { path: "", params: {} }, }, { input: "test/", expected: { path: "test/", params: { foo: "test" }, }, }, { input: "a/b.", expected: { path: "a/b.", params: { foo: "a", bar: "b" } }, }, ], }, { path: "/abc{abc:foo}", tests: [ { input: "/abc", expected: { path: "/abc", params: {} }, }, { input: "/abcabc", expected: false, }, { input: "/abcabc123", expected: { path: "/abcabc123", params: { foo: "123" } }, }, { input: "/abcabcabc123", expected: { path: "/abcabcabc123", params: { foo: "abc123" }, }, }, { input: "/abcabcabc", expected: { path: "/abcabcabc", params: { foo: "abc" } }, }, ], }, { path: "/:foo{abc:bar}", tests: [ { input: "/abc", expected: { params: { foo: "abc" }, path: "/abc", }, }, { input: "/abcabc", expected: { params: { foo: "abcabc" }, path: "/abcabc", }, }, { input: "/abcabc123", expected: { params: { foo: "abc", bar: "123" }, path: "/abcabc123", }, }, { input: "/acb", expected: { path: "/acb", params: { foo: "acb" }, }, }, { input: "/123", expected: { path: "/123", params: { foo: "123" }, }, }, { input: "/123abcabc", expected: { path: "/123abcabc", params: { foo: "123abcabc" }, }, }, ], }, { path: "/:foo\\abc:bar", tests: [ { input: "/abc", expected: false, }, { input: "/abcabc", expected: false, }, { input: "/abcabc123", expected: { path: "/abcabc123", params: { foo: "abc", bar: "123" }, }, }, { input: "/123abcabc", expected: false, }, ], }, { path: "/route|:param|", tests: [ { input: "/route|world|", expected: { path: "/route|world|", params: { param: "world" }, }, }, { input: "/route||", expected: false, }, ], }, { 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" }, }, }, { input: "/hello||", expected: { path: "/hello||", params: { foo: "hello||" } }, }, ], }, { path: ":foo\\@:bar", tests: [ { input: "x@y", expected: { path: "x@y", params: { foo: "x", bar: "y" } }, }, { input: "x@", expected: false, }, ], }, { path: "/*foo-:bar", tests: [ { input: "/a", expected: false, }, { input: "/a-b", expected: { path: "/a-b", params: { foo: ["a"], bar: "b" } }, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: ["a-b-c"], bar: "d" } }, }, ], }, { path: "/*foo-*bar-:baz", tests: [ { input: "/a", expected: false, }, { input: "/a-b-c", expected: { path: "/a-b-c", params: { foo: ["a"], bar: ["b"], baz: "c" }, }, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: ["a-b"], bar: ["c"], baz: "d" }, }, }, ], }, { path: "/*foo-:bar-*baz", tests: [ { input: "/a", expected: false, }, { input: "/a-b-c", expected: { path: "/a-b-c", params: { foo: ["a"], bar: "b", baz: ["c"] }, }, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: ["a-b"], bar: "c", baz: ["d"] }, }, }, ], }, { path: "/*foo-:bar-*baz-:qux", tests: [ { input: "/a", expected: false, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: ["a"], bar: "b", baz: ["c"], qux: "d" }, }, }, ], }, { path: "/*foo-*bar", tests: [ { input: "/a", expected: false, }, { input: "/a-b", expected: { path: "/a-b", params: { foo: ["a"], bar: ["b"] } }, }, { input: "/a-b-", expected: false, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: ["a-b-c"], bar: ["d"] } }, }, ], }, { path: "/:foo-:bar-*baz", tests: [ { input: "/a", expected: false, }, { input: "/a-b-c", expected: { path: "/a-b-c", params: { foo: "a", bar: "b", baz: ["c"] }, }, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: "a", bar: "b", baz: ["c-d"] }, }, }, ], }, { path: "/:foo-:bar-*baz-:qux", tests: [ { input: "/a", expected: false, }, { input: "/a-b-c", expected: false, }, { input: "/a-b-c-d", expected: { path: "/a-b-c-d", params: { foo: "a", bar: "b", baz: ["c"], qux: "d" }, }, }, { input: "/a-b-c-d-e", expected: { path: "/a-b-c-d-e", params: { foo: "a", bar: "b", baz: ["c-d"], qux: "e" }, }, }, { input: "/a-b-c/d-e-f", expected: { path: "/a-b-c/d-e-f", params: { foo: "a", bar: "b", baz: ["c", "d-e"], qux: "f" }, }, }, ], }, { path: "/*foo/:bar/*baz/:qux", tests: [ { input: "/a/b/c/d/e", expected: { path: "/a/b/c/d/e", params: { foo: ["a", "b"], bar: "c", baz: ["d"], qux: "e" }, }, }, ], }, { path: "/*foo/abc-:bar/xyz-*baz/:qux", tests: [ { input: "/a/b/c/d/e", expected: false, }, { input: "/a/abc-x/xyz-y/z", expected: { path: "/a/abc-x/xyz-y/z", params: { foo: ["a"], bar: "x", baz: ["y"], qux: "z" }, }, }, { input: "/a/abc-abc/xyz-/xyz-/z", expected: { path: "/a/abc-abc/xyz-/xyz-/z", params: { foo: ["a"], bar: "abc", baz: ["", "xyz-"], qux: "z" }, }, }, { input: "/a/abc-abc/xyz-/xyz-a/z", expected: { path: "/a/abc-abc/xyz-/xyz-a/z", params: { foo: ["a"], bar: "abc", baz: ["", "xyz-a"], qux: "z" }, }, }, { input: "/a/abc/abc-abc/xyz-/xyz-a/z", expected: { path: "/a/abc/abc-abc/xyz-/xyz-a/z", params: { foo: ["a", "abc"], bar: "abc", baz: ["", "xyz-a"], qux: "z", }, }, }, { input: "/abc-/abc-abc/xyz-/xyz-a/z", expected: { path: "/abc-/abc-abc/xyz-/xyz-a/z", params: { foo: ["abc-"], bar: "abc", baz: ["", "xyz-a"], qux: "z", }, }, }, ], }, { path: "/*a@:b-*c", tests: [ { input: "/foo@bar-baz", expected: { path: "/foo@bar-baz", params: { a: ["foo"], b: "bar", c: ["baz"] }, }, }, ], }, { path: "/:a-*b.:c@*d", tests: [ { input: "/a-b.c@d", expected: { path: "/a-b.c@d", params: { a: "a", b: ["b"], c: "c", d: ["d"] }, }, }, { input: "/a-b.c", expected: false, }, { input: "/a-b-c.d.e@f@g", expected: { path: "/a-b-c.d.e@f@g", params: { a: "a", b: ["b-c.d"], c: "e@f", d: ["g"] }, }, }, ], }, { path: "/*a--*b@@:c", tests: [ { input: "/a--b@@c", expected: { path: "/a--b@@c", params: { a: ["a"], b: ["b"], c: "c" } }, }, { input: "/a--b@@c/d", expected: false, }, { input: "/a--b/c@@d--e@@f", expected: { path: "/a--b/c@@d--e@@f", params: { a: ["a--b", "c@@d"], b: ["e"], c: "f" }, }, }, ], }, { path: "/*a~~:b~~*c/:d", tests: [ { input: "/a~~b~~c/d", expected: { path: "/a~~b~~c/d", params: { a: ["a"], b: "b", c: ["c"], d: "d" }, }, }, ], }, { path: "/*a--*b-.-:c", tests: [ { input: "/a--b-.-c", expected: { path: "/a--b-.-c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/*a~~*b._.:c", tests: [ { input: "/a~~b._.c", expected: { path: "/a~~b._.c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/*a@@*b~.~:c", tests: [ { input: "/a@@b~.~c", expected: { path: "/a@@b~.~c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/*a-.-*b@@:c", tests: [ { input: "/a-.-b@@c", expected: { path: "/a-.-b@@c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/*a@.@*b--:c", tests: [ { input: "/a@.@b--c", expected: { path: "/a@.@b--c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/*a-@-*b..:c", tests: [ { input: "/a-@-b..c", expected: { path: "/a-@-b..c", params: { a: ["a"], b: ["b"], c: "c" } }, }, ], }, { path: "/x/*a/*b/y", tests: [ { input: "/x/foo/bar/y", expected: { path: "/x/foo/bar/y", params: { a: ["foo"], b: ["bar"] } }, }, ], }, { path: "/x/*a-:b/*c/y", tests: [ { input: "/x/foo-bar/baz/y", expected: { path: "/x/foo-bar/baz/y", params: { a: ["foo"], b: "bar", c: ["baz"] }, }, }, { input: "/x/foo-bar/baz-qux/y", expected: { path: "/x/foo-bar/baz-qux/y", params: { a: ["foo"], b: "bar", c: ["baz-qux"] }, }, }, ], }, { path: "/x/*a-:b-:c/*d/y", tests: [ { input: "/x/foo-bar-baz/qux/y", expected: { path: "/x/foo-bar-baz/qux/y", params: { a: ["foo"], b: "bar", c: "baz", d: ["qux"] }, }, }, ], }, { path: "/x/*a-:b-:c-*d/*e/y", tests: [ { input: "/x/foo-bar-baz-qux/quux/y", expected: { path: "/x/foo-bar-baz-qux/quux/y", params: { a: ["foo"], b: "bar", c: "baz", d: ["qux"], e: ["quux"] }, }, }, ], }, { path: "/x/*a@/*c/y", tests: [ { input: "/x/foo@/y", expected: false, }, { input: "/x/foo@/y/y", expected: { path: "/x/foo@/y/y", params: { a: ["foo"], c: ["y"] } }, }, { input: "/x/foo@/y/z/y", expected: { path: "/x/foo@/y/z/y", params: { a: ["foo"], c: ["y", "z"] }, }, }, ], }, /** * Multi character delimiters. */ { path: "%25:foo{%25:bar}", options: { delimiter: "%25", }, tests: [ { input: "%25hello", expected: { path: "%25hello", params: { foo: "hello" } }, }, { input: "%25hello%25world", expected: { path: "%25hello%25world", params: { foo: "hello", bar: "world" }, }, }, { input: "%25555%25222", expected: { path: "%25555%25222", params: { foo: "555", bar: "222" }, }, }, ], }, { path: "%25:foo..:bar", options: { delimiter: "%25", }, tests: [ { input: "%25hello..world", expected: { path: "%25hello..world", params: { foo: "hello", bar: "world" }, }, }, { input: "%25555..222", expected: { path: "%25555..222", params: { foo: "555", bar: "222" }, }, }, { input: "%25555....222%25", expected: { path: "%25555....222%25", params: { foo: "555.", bar: ".222" }, }, }, ], }, /** * Array input is normalized. */ { path: ["/:foo/:bar", "/:foo/:baz"], tests: [ { input: "/hello/world", expected: { path: "/hello/world", params: { foo: "hello", bar: "world" }, }, }, ], }, { path: ["/x/*a/y/*b/z", "/x/*a/y/*b/w"], tests: [ { input: "/x/foo/y/bar/z", expected: { path: "/x/foo/y/bar/z", params: { a: ["foo"], b: ["bar"] }, }, }, { input: "/x/foo/y/bar/w", expected: { path: "/x/foo/y/bar/w", params: { a: ["foo"], b: ["bar"] }, }, }, ], }, { path: ["/x/*a/y-/*b/z", "/x/*a/y-/*b/w"], tests: [ { input: "/x/foo/y-/bar/z", expected: { path: "/x/foo/y-/bar/z", params: { a: ["foo"], b: ["bar"] }, }, }, { input: "/x/foo/y-/bar/w", expected: { path: "/x/foo/y-/bar/w", params: { a: ["foo"], b: ["bar"] }, }, }, ], }, /** * Token data. */ { path: { tokens: [ { type: "text", value: "/" }, { type: "param", name: "test" }, ], }, tests: [ { input: "/123", expected: { path: "/123", params: { test: "123" } } }, ], }, ]; pillarjs-path-to-regexp-34cdb0e/src/index.bench.ts000066400000000000000000000020531516127645000222150ustar00rootroot00000000000000import { 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); }); pillarjs-path-to-regexp-34cdb0e/src/index.spec.ts000066400000000000000000000145231516127645000220750ustar00rootroot00000000000000import { describe, it, expect } from "vitest"; import { parse, compile, match, stringify, pathToRegexp, TokenData, PathError, } from "./index.js"; import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS, STRINGIFY_TESTS, } from "./cases.spec.js"; /** * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { describe("ParseError", () => { it("should contain original path and debug url", () => { const error = new PathError( "Unexpected end at index 7, expected }", "/{:foo,", ); expect(error).toBeInstanceOf(TypeError); expect(error.message).toBe( "Unexpected end at index 7, expected }: /{:foo,; visit https://git.new/pathToRegexpError for info", ); expect(error.originalPath).toBe("/{:foo,"); }); it("should omit original url when undefined", () => { const error = new PathError( "Unexpected end at index 7, expected }", undefined, ); expect(error).toBeInstanceOf(TypeError); expect(error.message).toBe( "Unexpected end at index 7, expected }; visit https://git.new/pathToRegexpError for info", ); expect(error.originalPath).toBeUndefined(); }); }); describe("parse errors", () => { it("should throw on unbalanced group", () => { expect(() => parse("/{:foo,")).toThrow( new PathError("Unexpected end at index 7, expected }", "/{:foo,"), ); }); it("should throw on nested unbalanced group", () => { expect(() => parse("/{:foo/{x,y}")).toThrow( new PathError("Unexpected end at index 12, expected }", "/{:foo/{x,y}"), ); }); it("should throw on missing param name", () => { expect(() => parse("/:/")).toThrow( new PathError("Missing parameter name at index 2", "/:/"), ); }); it("should throw on missing wildcard name", () => { expect(() => parse("/*/")).toThrow( new PathError("Missing parameter name at index 2", "/*/"), ); }); it("should throw on unterminated quote", () => { expect(() => parse('/:"foo')).toThrow( new PathError("Unterminated quote at index 2", '/:"foo'), ); }); }); 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 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 when param gets an array", () => { const toPath = compile("/:foo"); expect(() => { toPath({ foo: [] }); }).toThrow(new TypeError('Expected "foo" to be a string')); }); 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')); }); }); describe("pathToRegexp errors", () => { it("should throw when missing text between params", () => { expect(() => pathToRegexp("/:foo:bar")).toThrow( new PathError('Missing text before "bar" param', "/:foo:bar"), ); }); it("should throw when missing text between params using TokenData", () => { expect(() => pathToRegexp( new TokenData([ { type: "param", name: "a" }, { type: "param", name: "b" }, ]), ), ).toThrow(new PathError('Missing text before "b" param', undefined)); }); it("should throw with `originalPath` when missing text between params using TokenData", () => { expect(() => pathToRegexp( new TokenData( [ { type: "param", name: "a" }, { type: "param", name: "b" }, ], "/[a][b]", ), ), ).toThrow(new PathError('Missing text before "b" param', "/[a][b]")); }); it("should contain the error line", () => { expect.hasAssertions(); try { pathToRegexp("/:"); } catch (error) { const stack = (error as Error).stack ?.split("\n") .slice(0, 5) .join("\n"); expect(stack).toContain("index.spec.ts"); } }); it("should throw when too many alternative routes are generated", () => { const segments = new Array(10).fill("{/x}").join(""); expect(() => pathToRegexp(segments)).toThrow( new PathError("Too many path combinations", segments), ); }); }); describe("stringify errors", () => { it("should error on unknown token", () => { expect(() => stringify({ tokens: [{ type: "unknown", value: "test" } as any] }), ).toThrow(new TypeError("Unknown token type: unknown")); }); }); describe.each(PARSER_TESTS)( "parse $path with $options", ({ path, options, expected }) => { it("should parse the path", () => { const data = parse(path, options); 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); }); }, ); describe.each(COMPILE_TESTS)( "compile $path with $options", ({ 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 }) => { it.each(tests)("should match $input", ({ input, expected }) => { const fn = match(path, options); expect(fn(input)).toEqual(expected); }); }, ); }); pillarjs-path-to-regexp-34cdb0e/src/index.ts000066400000000000000000000414251516127645000211450ustar00rootroot00000000000000const DEFAULT_DELIMITER = "/"; const NOOP_VALUE = (value: string) => value; const ID_START = /^[$_\p{ID_Start}]$/u; const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u; const ID = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u; /** * 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 { /** * A function for encoding input strings. */ encodePath?: Encode; } export interface PathToRegexpOptions { /** * Matches the path completely without trailing characters. (default: `true`) */ end?: boolean; /** * 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: `'/'`) */ 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`) */ encode?: Encode | false; /** * The default delimiter for segments. (default: `'/'`) */ delimiter?: string; } type TokenType = | "{" | "}" | "wildcard" | "param" | "char" | "escape" | "end" // Reserved for use or ambiguous due to past use. | "(" | ")" | "[" | "]" | "+" | "?" | "!"; /** * Tokenizer results. */ interface LexToken { type: TokenType; index: number; value: string; } const SIMPLE_TOKENS = "{}()[]+?!"; /** * Escape text for stringify to path. */ function escapeText(str: string) { return str.replace(/[{}()\[\]+?!:*\\]/g, "\\$&"); } /** * Escape a regular expression string. */ function escape(str: string) { return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); } /** * Plain text. */ export interface Text { type: "text"; value: 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 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. */ export type Token = Text | Parameter | Wildcard | Group; /** * Tokenized path instance. */ export class TokenData { constructor( public readonly tokens: Token[], public readonly originalPath?: string, ) {} } /** * ParseError is thrown when there is an error processing the path. */ export class PathError extends TypeError { constructor( message: string, public readonly originalPath: string | undefined, ) { let text = message; if (originalPath) text += `: ${originalPath}`; text += `; visit https://git.new/pathToRegexpError for info`; super(text); } } /** * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): TokenData { const { encodePath = NOOP_VALUE } = options; const chars = [...str]; const tokens: Array = []; let index = 0; let pos = 0; function name() { let value = ""; if (ID_START.test(chars[index])) { do { value += chars[index++]; } while (ID_CONTINUE.test(chars[index])); } else if (chars[index] === '"') { let quoteStart = index; while (index < chars.length) { if (chars[++index] === '"') { index++; quoteStart = 0; break; } // Increment over escape characters. if (chars[index] === "\\") index++; value += chars[index]; } if (quoteStart) { throw new PathError(`Unterminated quote at index ${quoteStart}`, str); } } if (!value) { throw new PathError(`Missing parameter name at index ${index}`, str); } return value; } while (index < chars.length) { const value = chars[index++]; if (SIMPLE_TOKENS.includes(value)) { tokens.push({ type: value as TokenType, index, value }); } else if (value === "\\") { tokens.push({ type: "escape", index, value: chars[index++] }); } else if (value === ":") { tokens.push({ type: "param", index, value: name() }); } else if (value === "*") { tokens.push({ type: "wildcard", index, value: name() }); } else { tokens.push({ type: "char", index, value }); } } tokens.push({ type: "end", index, value: "" }); function consumeUntil(endType: TokenType): Token[] { const output: Token[] = []; while (true) { const token = tokens[pos++]; if (token.type === endType) break; if (token.type === "char" || token.type === "escape") { let path = token.value; let cur = tokens[pos]; while (cur.type === "char" || cur.type === "escape") { path += cur.value; cur = tokens[++pos]; } output.push({ type: "text", value: encodePath(path), }); continue; } if (token.type === "param" || token.type === "wildcard") { output.push({ type: token.type, name: token.value, }); continue; } if (token.type === "{") { output.push({ type: "group", tokens: consumeUntil("}"), }); continue; } throw new PathError( `Unexpected ${token.type} at index ${token.index}, expected ${endType}`, str, ); } return output; } return new TokenData(consumeUntil("end"), str); } /** * Compile a string to a template function for the path. */ export function compile

( path: Path, options: CompileOptions & ParseOptions = {}, ) { const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = options; const data = typeof path === "object" ? path : parse(path, options); const fn = tokensToFunction(data.tokens, delimiter, encode); return function path(params: P = {} as P) { const [path, ...missing] = fn(params); if (missing.length) { throw new TypeError(`Missing parameters: ${missing.join(", ")}`); } return path; }; } export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; function tokensToFunction( tokens: Token[], delimiter: string, encode: Encode | false, ) { const encoders = tokens.map((token) => tokenToFunction(token, delimiter, encode), ); 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 tokenToFunction( token: Token, delimiter: string, encode: Encode | false, ): (data: ParamData) => string[] { if (token.type === "text") return () => [token.value]; if (token.type === "group") { const fn = tokensToFunction(token.tokens, delimiter, encode); return (data) => { const [value, ...missing] = fn(data); if (!missing.length) return [value]; return [""]; }; } const encodeValue = encode || NOOP_VALUE; if (token.type === "wildcard" && encode !== false) { return (data) => { const value = data[token.name]; if (value == null) return ["", token.name]; if (!Array.isArray(value) || value.length === 0) { throw new TypeError(`Expected "${token.name}" to be a non-empty array`); } 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) => { const value = data[token.name]; if (value == null) return ["", token.name]; if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}" to be a string`); } return [encodeValue(value)]; }; } /** * A match result contains data about the path match. */ export interface MatchResult

{ path: string; params: P; } /** * A match is either `false` (no match) or a match result. */ export type Match

= false | MatchResult

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

= (path: string) => Match

; /** * Supported path types. */ export type Path = string | TokenData; /** * 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; if (key.type === "param") return decode; return (value: string) => value.split(delimiter).map(decode); }); return function match(input: string) { const m = regexp.exec(input); if (!m) return false; const path = m[0]; const params = Object.create(null); 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]); } return { path, params }; }; } /** * Transform a path into a regular expression and capture keys. */ export function pathToRegexp( path: Path | Path[], options: PathToRegexpOptions & ParseOptions = {}, ) { const { delimiter = DEFAULT_DELIMITER, end = true, sensitive = false, trailing = true, } = options; const root = new SourceNode("^"); const paths: Array = [path]; let combinations = 0; while (paths.length) { const path = paths.shift()!; if (Array.isArray(path)) { paths.push(...path); continue; } const data = typeof path === "object" ? path : parse(path, options); flatten(data.tokens, 0, [], (tokens) => { if (combinations++ >= 256) { throw new PathError("Too many path combinations", data.originalPath); } let node = root; for (const part of toRegExpSource(tokens, delimiter, data.originalPath)) { node = node.add(part.source, part.key); } node.add(""); // Mark the end of the source. }); } const keys: Keys = []; let pattern = toRegExp(root, keys); if (trailing) pattern += "(?:" + escape(delimiter) + "$)?"; pattern += end ? "$" : "(?=" + escape(delimiter) + "|$)"; return { regexp: new RegExp(pattern, sensitive ? "" : "i"), keys }; } function toRegExp(node: SourceNode, keys: Keys): string { if (node.key) keys.push(node.key); const children = Object.keys(node.children); const text = children .map((id) => toRegExp(node.children[id], keys)) .join("|"); return node.source + (children.length < 2 ? text : `(?:${text})`); } class SourceNode { children: Record = Object.create(null); constructor( public source: string, public key?: Key, ) {} add(source: string, key?: Key) { const id = source + ":" + (key ? key.name : ""); return (this.children[id] ||= new SourceNode(source, key)); } } /** * Generate a flat list of sequence tokens from the given tokens. */ function flatten( tokens: Token[], index: number, result: Exclude[], callback: (result: Exclude[]) => void, ): void { while (index < tokens.length) { const token = tokens[index++]; if (token.type === "group") { flatten(token.tokens, 0, result.slice(), (seq) => flatten(tokens, index, seq, callback), ); continue; } result.push(token); } callback(result); } /** * Simplest token for the trie deduplication. */ interface RegExpPart { source: string; key?: Key; } /** * Transform a flat sequence of tokens into a regular expression. */ function toRegExpSource( tokens: Exclude[], delimiter: string, originalPath: string | undefined, ): RegExpPart[] { let result: RegExpPart[] = []; let backtrack = ""; let wildcardBacktrack = ""; let prevCaptureType: 0 | 1 | 2 = 0; let hasSegmentCapture = 0; let index = 0; function hasInSegment(index: number, type: TokenType) { while (index < tokens.length) { const token = tokens[index++]; if (token.type === type) return true; if (token.type === "text") { if (token.value.includes(delimiter)) break; } } return false; } function peekText(index: number) { let result = ""; while (index < tokens.length) { const token = tokens[index++]; if (token.type !== "text") break; result += token.value; } return result; } while (index < tokens.length) { const token = tokens[index++]; if (token.type === "text") { result.push({ source: escape(token.value) }); backtrack += token.value; if (prevCaptureType === 2) wildcardBacktrack += token.value; if (token.value.includes(delimiter)) hasSegmentCapture = 0; continue; } if (token.type === "param" || token.type === "wildcard") { if (prevCaptureType && !backtrack) { throw new PathError( `Missing text before "${token.name}" ${token.type}`, originalPath, ); } if (token.type === "param") { result.push({ source: hasSegmentCapture // Seen param/wildcard in segment. ? `(${negate(delimiter, backtrack)}+?)` : hasInSegment(index, "wildcard") // See wildcard later in segment. ? `(${negate(delimiter, peekText(index))}+?)` : `(${negate(delimiter, "")}+?)`, key: token, }); hasSegmentCapture |= prevCaptureType = 1; } else { result.push({ source: hasSegmentCapture & 2 // Seen wildcard in segment. ? `(${negate(backtrack, "")}+?)` : hasSegmentCapture & 1 // Seen param in segment. ? `(${negate(wildcardBacktrack, "")}+?)` : wildcardBacktrack // No capture in segment, seen wildcard in path. ? `(${negate(wildcardBacktrack, "")}+?|${negate(delimiter, "")}+?)` : `([^]+?)`, key: token, }); wildcardBacktrack = ""; hasSegmentCapture |= prevCaptureType = 2; } backtrack = ""; continue; } throw new TypeError(`Unknown token type: ${(token as any).type}`); } return result; } /** * Block backtracking on previous text/delimiter. */ function negate(a: string, b: string): string { if (b.length > a.length) return negate(b, a); // Longest string first. if (a === b) b = ""; // Cleaner regex strings, no duplication. if (b.length > 1) return `(?:(?!${escape(a)}|${escape(b)})[^])`; if (a.length > 1) return `(?:(?!${escape(a)})[^${escape(b)}])`; return `[^${escape(a + b)}]`; } /** * Stringify an array of tokens into a path string. */ function stringifyTokens(tokens: Token[], index: number): string { let value = ""; while (index < tokens.length) { const token = tokens[index++]; if (token.type === "text") { value += escapeText(token.value); continue; } if (token.type === "group") { value += "{" + stringifyTokens(token.tokens, 0) + "}"; continue; } if (token.type === "param") { value += ":" + stringifyName(token.name, tokens[index]); continue; } if (token.type === "wildcard") { value += "*" + stringifyName(token.name, tokens[index]); continue; } throw new TypeError(`Unknown token type: ${(token as any).type}`); } return value; } /** * Stringify token data into a path string. */ export function stringify(data: TokenData): string { return stringifyTokens(data.tokens, 0); } /** * Stringify a parameter name, escaping when it cannot be emitted directly. */ function stringifyName(name: string, next: Token | undefined): string { if (!ID.test(name)) return JSON.stringify(name); if (next?.type === "text" && ID_CONTINUE.test(next.value[0])) { return JSON.stringify(name); } return name; } pillarjs-path-to-regexp-34cdb0e/src/redos.spec.ts000066400000000000000000000007261516127645000221020ustar00rootroot00000000000000import { check } from "recheck"; import { pathToRegexp } from "./index.js"; import { MATCH_TESTS } from "./cases.spec.js"; import { describe, expect, it } from "vitest"; describe.concurrent("redos", () => { it.each(MATCH_TESTS.map((x) => [x.path, pathToRegexp(x.path).regexp]))( "%s - %s", { timeout: 10_000 }, async (_, regexp) => { const result = await check(regexp.source, regexp.flags); expect(result.status).toBe("safe"); }, ); }); pillarjs-path-to-regexp-34cdb0e/tsconfig.build.json000066400000000000000000000002071516127645000224750ustar00rootroot00000000000000{ "extends": "./tsconfig.json", "compilerOptions": { "types": [] }, "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"] } pillarjs-path-to-regexp-34cdb0e/tsconfig.json000066400000000000000000000004461516127645000214040ustar00rootroot00000000000000{ "extends": "@borderless/ts-scripts/configs/tsconfig.json", "compilerOptions": { "target": "ES2015", "lib": ["ES2015"], "rootDir": "src", "outDir": "dist", "module": "NodeNext", "moduleResolution": "NodeNext", "types": ["node"] }, "include": ["src/**/*"] } pillarjs-path-to-regexp-34cdb0e/vitest.config.mts000066400000000000000000000003161516127645000222000ustar00rootroot00000000000000/// import { defineConfig } from "vite"; export default defineConfig({ test: { coverage: { include: ["src/**/*"], exclude: ["**/*.bench.ts"], }, }, });