Full Code of pillarjs/path-to-regexp for AI

master 05a5a973702a cached
17 files
77.2 KB
21.7k tokens
61 symbols
1 requests
Download .txt
Repository: pillarjs/path-to-regexp
Branch: master
Commit: 05a5a973702a
Files: 17
Total size: 77.2 KB

Directory structure:
gitextract_ed45vrg3/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── codeql.yml
│       └── scorecard.yml
├── .gitignore
├── History.md
├── LICENSE
├── Readme.md
├── package.json
├── scripts/
│   └── redos.ts
├── src/
│   ├── cases.spec.ts
│   ├── index.bench.ts
│   ├── index.spec.ts
│   └── index.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.mts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# 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


================================================
FILE: .github/workflows/ci.yml
================================================
name: 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 }}


================================================
FILE: .github/workflows/codeql.yml
================================================
# 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}}"


================================================
FILE: .github/workflows/scorecard.yml
================================================
# 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


================================================
FILE: .gitignore
================================================
.vscode/
node_modules/
coverage/
dist/
dist.es2015/
*.tsbuildinfo
package-lock.json


================================================
FILE: History.md
================================================
# 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


================================================
FILE: LICENSE
================================================
The 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.


================================================
FILE: Readme.md
================================================
# 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


================================================
FILE: package.json
================================================
{
  "name": "path-to-regexp",
  "version": "8.3.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.4.5",
    "size-limit": "^11.1.2",
    "typescript": "^5.7.3",
    "vitest": "^3.0.5"
  },
  "publishConfig": {
    "access": "public"
  },
  "size-limit": [
    {
      "path": "dist/index.js",
      "limit": "2 kB"
    }
  ],
  "ts-scripts": {
    "dist": [
      "dist"
    ],
    "project": [
      "tsconfig.build.json"
    ]
  }
}


================================================
FILE: scripts/redos.ts
================================================
import { checkSync } from "recheck";
import { pathToRegexp } from "../src/index.js";
import { MATCH_TESTS } from "../src/cases.spec.js";

let safe = 0;
let fail = 0;

const TESTS = MATCH_TESTS.map((x) => x.path);

for (const path of TESTS) {
  const { regexp } = pathToRegexp(path);
  const result = checkSync(regexp.source, regexp.flags);
  if (result.status === "safe") {
    safe++;
    console.log("Safe:", path, String(regexp));
  } else {
    fail++;
    console.log("Fail:", path, String(regexp));
  }
}

console.log("Safe:", safe, "Fail:", fail);


================================================
FILE: src/cases.spec.ts
================================================
import {
  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<any>;
  }>;
}

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,
      },
    ],
  },

  /**
   * 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" },
        },
      },
    ],
  },

  /**
   * Token data.
   */
  {
    path: {
      tokens: [
        { type: "text", value: "/" },
        { type: "param", name: "test" },
      ],
    },
    tests: [
      { input: "/123", expected: { path: "/123", params: { test: "123" } } },
    ],
  },
];


================================================
FILE: src/index.bench.ts
================================================
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);
});


================================================
FILE: src/index.spec.ts
================================================
import { 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");
      }
    });
  });

  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);
      });
    },
  );
});


================================================
FILE: src/index.ts
================================================
const DEFAULT_DELIMITER = "/";
const NOOP_VALUE = (value: string) => value;
const ID_START = /^[$_\p{ID_Start}]$/u;
const ID_CONTINUE = /^[$\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: Record<string, TokenType> = {
  // Groups.
  "{": "{",
  "}": "}",
  // Reserved.
  "(": "(",
  ")": ")",
  "[": "[",
  "]": "]",
  "+": "+",
  "?": "?",
  "!": "!",
};

/**
 * 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<Key>;

/**
 * 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<LexToken> = [];
  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];
    const type = SIMPLE_TOKENS[value];

    if (type) {
      tokens.push({ type, index: index++, value });
    } else if (value === "\\") {
      tokens.push({ type: "escape", index: index++, value: chars[index++] });
    } else if (value === ":") {
      tokens.push({ type: "param", index: index++, value: name() });
    } else if (value === "*") {
      tokens.push({ type: "wildcard", index: index++, value: name() });
    } else {
      tokens.push({ type: "char", index: 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<P extends ParamData = ParamData>(
  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<Record<string, string | string[]>>;
export type PathFunction<P extends ParamData> = (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<P extends ParamData> {
  path: string;
  params: P;
}

/**
 * A match is either `false` (no match) or a match result.
 */
export type Match<P extends ParamData> = false | MatchResult<P>;

/**
 * The match function takes a string and returns whether it matched the path.
 */
export type MatchFunction<P extends ParamData> = (path: string) => Match<P>;

/**
 * Supported path types.
 */
export type Path = string | TokenData;

/**
 * Transform a path into a match function.
 */
export function match<P extends ParamData>(
  path: Path | Path[],
  options: MatchOptions & ParseOptions = {},
): MatchFunction<P> {
  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 };
  };
}

export function pathToRegexp(
  path: Path | Path[],
  options: PathToRegexpOptions & ParseOptions = {},
) {
  const {
    delimiter = DEFAULT_DELIMITER,
    end = true,
    sensitive = false,
    trailing = true,
  } = options;
  const keys: Keys = [];
  const flags = sensitive ? "" : "i";
  const sources: string[] = [];

  for (const input of pathsToArray(path, [])) {
    const data = typeof input === "object" ? input : parse(input, options);
    for (const tokens of flatten(data.tokens, 0, [])) {
      sources.push(toRegExpSource(tokens, delimiter, keys, data.originalPath));
    }
  }

  let pattern = `^(?:${sources.join("|")})`;
  if (trailing) pattern += `(?:${escape(delimiter)}$)?`;
  pattern += end ? "$" : `(?=${escape(delimiter)}|$)`;

  const regexp = new RegExp(pattern, flags);
  return { regexp, keys };
}

/**
 * Convert a path or array of paths into a flat array.
 */
function pathsToArray(paths: Path | Path[], init: Path[]): Path[] {
  if (Array.isArray(paths)) {
    for (const p of paths) pathsToArray(p, init);
  } else {
    init.push(paths);
  }
  return init;
}

/**
 * Flattened token set.
 */
type FlatToken = Text | Parameter | Wildcard;

/**
 * Generate a flat list of sequence tokens from the given tokens.
 */
function* flatten(
  tokens: Token[],
  index: number,
  init: FlatToken[],
): Generator<FlatToken[]> {
  if (index === tokens.length) {
    return yield init;
  }

  const token = tokens[index];

  if (token.type === "group") {
    for (const seq of flatten(token.tokens, 0, init.slice())) {
      yield* flatten(tokens, index + 1, seq);
    }
  } else {
    init.push(token);
  }

  yield* flatten(tokens, index + 1, init);
}

/**
 * Transform a flat sequence of tokens into a regular expression.
 */
function toRegExpSource(
  tokens: FlatToken[],
  delimiter: string,
  keys: Keys,
  originalPath: string | undefined,
): string {
  let result = "";
  let backtrack = "";
  let isSafeSegmentParam = true;

  for (const token of tokens) {
    if (token.type === "text") {
      result += escape(token.value);
      backtrack += token.value;
      isSafeSegmentParam ||= token.value.includes(delimiter);
      continue;
    }

    if (token.type === "param" || token.type === "wildcard") {
      if (!isSafeSegmentParam && !backtrack) {
        throw new PathError(
          `Missing text before "${token.name}" ${token.type}`,
          originalPath,
        );
      }

      if (token.type === "param") {
        result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`;
      } else {
        result += `([\\s\\S]+)`;
      }

      keys.push(token);
      backtrack = "";
      isSafeSegmentParam = false;
      continue;
    }
  }

  return result;
}

/**
 * Block backtracking on previous text and ignore delimiter string.
 */
function negate(delimiter: string, backtrack: string): string {
  if (backtrack.length < 2) {
    if (delimiter.length < 2) return `[^${escape(delimiter + backtrack)}]`;
    return `(?:(?!${escape(delimiter)})[^${escape(backtrack)}])`;
  }
  if (delimiter.length < 2) {
    return `(?:(?!${escape(backtrack)})[^${escape(delimiter)}])`;
  }
  return `(?:(?!${escape(backtrack)}|${escape(delimiter)})[\\s\\S])`;
}

/**
 * Stringify an array of tokens into a path string.
 */
function stringifyTokens(tokens: Token[]): string {
  let value = "";
  let i = 0;

  function name(value: string) {
    const isSafe = isNameSafe(value) && isNextNameSafe(tokens[i]);
    return isSafe ? value : JSON.stringify(value);
  }

  while (i < tokens.length) {
    const token = tokens[i++];

    if (token.type === "text") {
      value += escapeText(token.value);
      continue;
    }

    if (token.type === "group") {
      value += `{${stringifyTokens(token.tokens)}}`;
      continue;
    }

    if (token.type === "param") {
      value += `:${name(token.name)}`;
      continue;
    }

    if (token.type === "wildcard") {
      value += `*${name(token.name)}`;
      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);
}

/**
 * Validate the parameter name contains valid ID characters.
 */
function isNameSafe(name: string): boolean {
  const [first, ...rest] = name;
  return ID_START.test(first) && rest.every((char) => ID_CONTINUE.test(char));
}

/**
 * Validate the next token does not interfere with the current param name.
 */
function isNextNameSafe(token: Token | undefined): boolean {
  if (token && token.type === "text") return !ID_CONTINUE.test(token.value[0]);
  return true;
}


================================================
FILE: tsconfig.build.json
================================================
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": []
  },
  "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"]
}


================================================
FILE: tsconfig.json
================================================
{
  "extends": "@borderless/ts-scripts/configs/tsconfig.json",
  "compilerOptions": {
    "target": "ES2015",
    "lib": ["ES2015"],
    "rootDir": "src",
    "outDir": "dist",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "types": ["node"]
  },
  "include": ["src/**/*"]
}


================================================
FILE: vitest.config.mts
================================================
/// <reference types="vitest" />
import { defineConfig } from "vite";

export default defineConfig({
  test: {
    coverage: {
      include: ["src/**/*"],
      exclude: ["**/*.bench.ts"],
    },
  },
});
Download .txt
gitextract_ed45vrg3/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── codeql.yml
│       └── scorecard.yml
├── .gitignore
├── History.md
├── LICENSE
├── Readme.md
├── package.json
├── scripts/
│   └── redos.ts
├── src/
│   ├── cases.spec.ts
│   ├── index.bench.ts
│   ├── index.spec.ts
│   └── index.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.mts
Download .txt
SYMBOL INDEX (61 symbols across 4 files)

FILE: scripts/redos.ts
  constant TESTS (line 8) | const TESTS = MATCH_TESTS.map((x) => x.path);

FILE: src/cases.spec.ts
  type ParserTestSet (line 12) | interface ParserTestSet {
  type StringifyTestSet (line 18) | interface StringifyTestSet {
  type CompileTestSet (line 24) | interface CompileTestSet {
  type MatchTestSet (line 33) | interface MatchTestSet {
  constant PARSER_TESTS (line 42) | const PARSER_TESTS: ParserTestSet[] = [
  constant STRINGIFY_TESTS (line 151) | const STRINGIFY_TESTS: StringifyTestSet[] = [
  constant COMPILE_TESTS (line 236) | const COMPILE_TESTS: CompileTestSet[] = [
  constant MATCH_TESTS (line 350) | const MATCH_TESTS: MatchTestSet[] = [

FILE: src/index.bench.ts
  constant PATHS (line 4) | const PATHS: string[] = [
  constant STATIC_PATH_MATCH (line 13) | const STATIC_PATH_MATCH = match("/user");
  constant SIMPLE_PATH_MATCH (line 14) | const SIMPLE_PATH_MATCH = match("/user/:id");
  constant MULTI_SEGMENT_MATCH (line 15) | const MULTI_SEGMENT_MATCH = match("/:x/:y");
  constant MULTI_PATTERN_MATCH (line 16) | const MULTI_PATTERN_MATCH = match("/:x-:y");
  constant TRICKY_PATTERN_MATCH (line 17) | const TRICKY_PATTERN_MATCH = match("/:foo|:bar|");
  constant ASTERISK_MATCH (line 18) | const ASTERISK_MATCH = match("/*foo");

FILE: src/index.ts
  constant DEFAULT_DELIMITER (line 1) | const DEFAULT_DELIMITER = "/";
  constant ID_START (line 3) | const ID_START = /^[$_\p{ID_Start}]$/u;
  constant ID_CONTINUE (line 4) | const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u;
  type Encode (line 9) | type Encode = (value: string) => string;
  type Decode (line 14) | type Decode = (value: string) => string;
  type ParseOptions (line 16) | interface ParseOptions {
  type PathToRegexpOptions (line 23) | interface PathToRegexpOptions {
  type MatchOptions (line 42) | interface MatchOptions extends PathToRegexpOptions {
  type CompileOptions (line 49) | interface CompileOptions {
  type TokenType (line 60) | type TokenType =
  type LexToken (line 80) | interface LexToken {
  constant SIMPLE_TOKENS (line 86) | const SIMPLE_TOKENS: Record<string, TokenType> = {
  function escapeText (line 103) | function escapeText(str: string) {
  function escape (line 110) | function escape(str: string) {
  type Text (line 117) | interface Text {
  type Parameter (line 125) | interface Parameter {
  type Wildcard (line 133) | interface Wildcard {
  type Group (line 141) | interface Group {
  type Key (line 149) | type Key = Parameter | Wildcard;
  type Keys (line 154) | type Keys = Array<Key>;
  type Token (line 159) | type Token = Text | Parameter | Wildcard | Group;
  class TokenData (line 164) | class TokenData {
    method constructor (line 165) | constructor(
  class PathError (line 174) | class PathError extends TypeError {
    method constructor (line 175) | constructor(
  function parse (line 189) | function parse(str: string, options: ParseOptions = {}): TokenData {
  function compile (line 304) | function compile<P extends ParamData = ParamData>(
  type ParamData (line 322) | type ParamData = Partial<Record<string, string | string[]>>;
  type PathFunction (line 323) | type PathFunction<P extends ParamData> = (data?: P) => string;
  function tokensToFunction (line 325) | function tokensToFunction(
  function tokenToFunction (line 350) | function tokenToFunction(
  type MatchResult (line 409) | interface MatchResult<P extends ParamData> {
  type Match (line 417) | type Match<P extends ParamData> = false | MatchResult<P>;
  type MatchFunction (line 422) | type MatchFunction<P extends ParamData> = (path: string) => Match<P>;
  type Path (line 427) | type Path = string | TokenData;
  function match (line 432) | function match<P extends ParamData>(
  function pathToRegexp (line 465) | function pathToRegexp(
  function pathsToArray (line 497) | function pathsToArray(paths: Path | Path[], init: Path[]): Path[] {
  type FlatToken (line 509) | type FlatToken = Text | Parameter | Wildcard;
  function toRegExpSource (line 539) | function toRegExpSource(
  function negate (line 584) | function negate(delimiter: string, backtrack: string): string {
  function stringifyTokens (line 598) | function stringifyTokens(tokens: Token[]): string {
  function stringify (line 639) | function stringify(data: TokenData): string {
  function isNameSafe (line 646) | function isNameSafe(name: string): boolean {
  function isNextNameSafe (line 654) | function isNextNameSafe(token: Token | undefined): boolean {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (86K chars).
[
  {
    "path": ".editorconfig",
    "chars": 188,
    "preview": "# EditorConfig: http://EditorConfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = tr"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 536,
    "preview": "name: CI\non:\n  - push\n  - pull_request\npermissions:\n  contents: read\njobs:\n  test:\n    name: Node.js ${{ matrix.node-ver"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 2530,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "chars": 2988,
    "preview": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by "
  },
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": ".vscode/\nnode_modules/\ncoverage/\ndist/\ndist.es2015/\n*.tsbuildinfo\npackage-lock.json\n"
  },
  {
    "path": "History.md",
    "chars": 4930,
    "preview": "# Moved to [GitHub Releases](https://github.com/pillarjs/path-to-regexp/releases)\n\n## 3.0.0 / 2019-01-13\n\n- Always use p"
  },
  {
    "path": "LICENSE",
    "chars": 1103,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Blake Embrey (hello@blakeembrey.com)\n\nPermission is hereby granted, free of ch"
  },
  {
    "path": "Readme.md",
    "chars": 7849,
    "preview": "# Path-to-RegExp\n\n> Turn a path string such as `/user/:name` into a regular expression.\n\n[![NPM version][npm-image]][npm"
  },
  {
    "path": "package.json",
    "chars": 1399,
    "preview": "{\n  \"name\": \"path-to-regexp\",\n  \"version\": \"8.3.0\",\n  \"description\": \"Express style path to RegExp utility\",\n  \"keywords"
  },
  {
    "path": "scripts/redos.ts",
    "chars": 555,
    "preview": "import { checkSync } from \"recheck\";\nimport { pathToRegexp } from \"../src/index.js\";\nimport { MATCH_TESTS } from \"../src"
  },
  {
    "path": "src/cases.spec.ts",
    "chars": 33842,
    "preview": "import {\n  type MatchOptions,\n  type Match,\n  type ParseOptions,\n  type Token,\n  type CompileOptions,\n  type ParamData,\n"
  },
  {
    "path": "src/index.bench.ts",
    "chars": 1067,
    "preview": "import { bench } from \"vitest\";\nimport { match } from \"./index.js\";\n\nconst PATHS: string[] = [\n  \"/xyz\",\n  \"/user\",\n  \"/"
  },
  {
    "path": "src/index.spec.ts",
    "chars": 6211,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport {\n  parse,\n  compile,\n  match,\n  stringify,\n  pathToRegexp,\n  Toke"
  },
  {
    "path": "src/index.ts",
    "chars": 15101,
    "preview": "const DEFAULT_DELIMITER = \"/\";\nconst NOOP_VALUE = (value: string) => value;\nconst ID_START = /^[$_\\p{ID_Start}]$/u;\ncons"
  },
  {
    "path": "tsconfig.build.json",
    "chars": 135,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": []\n  },\n  \"exclude\": [\"src/**/*.spec.ts\", \"src/**/"
  },
  {
    "path": "tsconfig.json",
    "chars": 294,
    "preview": "{\n  \"extends\": \"@borderless/ts-scripts/configs/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2015\",\n    \"lib\":"
  },
  {
    "path": "vitest.config.mts",
    "chars": 206,
    "preview": "/// <reference types=\"vitest\" />\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  test: {\n    cover"
  }
]

About this extraction

This page contains the full source code of the pillarjs/path-to-regexp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (77.2 KB), approximately 21.7k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!