Repository: kenchris/urlpattern-polyfill
Branch: main
Commit: f147a0f42a94
Files: 30
Total size: 194.0 KB
Directory structure:
gitextract_r56z8t2v/
├── .github/
│ └── workflows/
│ └── workflow.yml
├── .gitignore
├── LICENSE
├── README.md
├── index.cjs
├── index.html
├── index.js
├── package.json
├── src/
│ ├── LICENSE.path-to-regex
│ ├── index.d.ts
│ ├── parseShorthand.ts
│ ├── path-to-regex-modified.ts
│ ├── types.d.ts
│ ├── url-pattern-list.ts
│ ├── url-pattern-parser.ts
│ ├── url-pattern.interfaces.ts
│ ├── url-pattern.ts
│ └── url-utils.ts
├── test/
│ ├── can-load-as-cms.cjs
│ ├── can-load-as-esm.mjs
│ ├── can-load-as-ts.ts
│ ├── ponyfill.cjs
│ ├── ponyfill.mjs
│ ├── urlpattern-compare-test-data.json
│ ├── urlpattern-compare-tests.js
│ ├── urlpattern-hasregexpgroups-tests.js
│ ├── urlpatterntestdata.json
│ └── urlpatterntests.js
├── tsconfig.json
└── urlpattern/
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/workflow.yml
================================================
name: run-tests
on:
push:
branches:
- master
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: npm install --no-package-lock
- name: Test
run: npm test
================================================
FILE: .gitignore
================================================
node_modules
dist
.vscode
package-lock.json
coverage
.wireit
================================================
FILE: LICENSE
================================================
Copyright 2020 Intel Corporation
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
================================================
[](https://github.com/kenchris/urlpattern-polyfill/actions/workflows/workflow.yml)
[](https://www.npmjs.com/package/urlpattern-polyfill)

URLPattern polyfills
===
URLPattern is a new web API for matching URLs. Its intended to both provide a convenient API for web developers and to be usable in other web APIs that need to match URLs; e.g. service workers. The [explainer](https://github.com/wanderview/service-worker-scope-pattern-matching/blob/master/explainer.md) discusses the motivating use cases.
This is a polyfill for the [URLPattern API](https://wicg.github.io/urlpattern/) so that the feature is available in browsers that don't support it natively. This polyfill passes
the same web platform test suite.
How to load the polyfill
---
The polyfill works in browsers (ESM module) and in Node.js either via import (ESM module) or via require (CJS module).
The polyfill will only be loaded if the URLPattern doesn't already exist on the global object, and in that case it will add it to the global object.
## loading as ESM module
```javascript
// Conditional ESM module loading (Node.js and browser)
// @ts-ignore: Property 'UrlPattern' does not exist
if (!globalThis.URLPattern) {
await import("urlpattern-polyfill");
}
/**
* The above is the recommended way to load the ESM module, as it only
* loads it on demand, thus when not natively supported by the runtime or
* already polyfilled.
*/
import "urlpattern-polyfill";
/**
* In case you want to replace an existing implementation with the polyfill:
*/
import {URLPattern} from "urlpattern-polyfill";
globalThis.URLPattern = URLPattern
```
> ## Note:
> The line with `// @ts-ignore: Property 'UrlPattern' does not exist ` is needed in some environments because before you load the polyfill it might not be available, and the feature-check in the if statement gives an TypeScript error. The whole idea is that it loads when its not there.
## loading as CommonJs module
```javascript
// Conditional CJS module loading (Node.js)
if (!globalThis.URLPattern) {
require("urlpattern-polyfill");
}
/**
* The above is the recommended way to load the CommonJs module, as it only
* loads it on demand, thus when not natively supported by the runtime or
* already polyfilled.
*/
require("urlpattern-polyfill");
/**
* In case you want to replace an existing implementation with the polyfill:
*/
const {URLPattern} = require("urlpattern-polyfill");;
globalThis.URLPattern = URLPattern
```
> ## Note:
> No matter how you load the polyfill, when there is no implementation in your environment, it will _always_ add it to the global object.
Basic example
---
```javascript
let p = new URLPattern({ pathname: '/foo/:name' });
let r = p.exec('https://example.com/foo/bar');
console.log(r.pathname.input); // "/foo/bar"
console.log(r.pathname.groups.name); // "bar"
let r2 = p.exec({ pathname: '/foo/baz' });
console.log(r2.pathname.groups.name); // "baz"
```
Example of matching same-origin JPG or PNG requests
---
```javascript
// Match same-origin jpg or png URLs.
// Note: This uses a named group to make it easier to access
// the result later.
const p = new URLPattern({
pathname: '/*.:filetype(jpg|png)',
baseURL: self.location
});
for (let url in url_list) {
const r = p.exec(url);
// skip non-matches
if (!r) {
continue;
}
if (r.pathname.groups['filetype'] === 'jpg') {
// process jpg
} else if (r.pathname.groups['filetype'] === 'png') {
// process png
}
}
```
The pattern in this case can be made simpler without the origin check by leaving off the baseURL.
```javascript
// Match any URL ending with 'jpg' or 'png'.
const p = new URLPattern({ pathname: '/*.:filetype(jpg|png)' });
```
Example of Short Form Support
---
We are planning to also support a "short form" for initializing URLPattern objects.
This is supported by the polyfill but not yet by the Chromium implementation.
For example:
```javascript
const p = new URLPattern("https://*.example.com/foo/*");
```
Or:
```javascript
const p = new URLPattern("foo/*", self.location);
```
API reference
===
API overview with typeScript type annotations is found below. Associated browser Web IDL can be found [here](https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/modules/url_pattern/).
```ts
type URLPatternInput = URLPatternInit | string;
class URLPattern {
constructor(init?: URLPatternInput, baseURL?: string);
test(input?: URLPatternInput, baseURL?: string): boolean;
exec(input?: URLPatternInput, baseURL?: string): URLPatternResult | null;
readonly protocol: string;
readonly username: string;
readonly password: string;
readonly hostname: string;
readonly port: string;
readonly pathname: string;
readonly search: string;
readonly hash: string;
}
interface URLPatternInit {
baseURL?: string;
username?: string;
password?: string;
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
hash?: string;
}
interface URLPatternResult {
inputs: [URLPatternInput];
protocol: URLPatternComponentResult;
username: URLPatternComponentResult;
password: URLPatternComponentResult;
hostname: URLPatternComponentResult;
port: URLPatternComponentResult;
pathname: URLPatternComponentResult;
search: URLPatternComponentResult;
hash: URLPatternComponentResult;
}
interface URLPatternComponentResult {
input: string;
groups: {
[key: string]: string | undefined;
};
}
```
Pattern syntax
===
The pattern syntax here is based on what is used in the popular path-to-regexp library.
* An understanding of a "divider" that separates segments of the string. For the pathname this is typically the `"/"` character.
* A regex group defined by an enclosed set of parentheses. Inside of the parentheses a general regex may be defined.
* A named group that matches characters until the next divider. The named group begins with a `":"` character and then a name. For example, `"/:foo/:bar"` has two named groups.
* A custom regex for a named group. In this case a set of parentheses with a regex immediately follows the named group; e.g. `"/:foo(.*)"` will override the default of matching to the next divider.
* A modifier may optionally follow a regex or named group. A modifier is a `"?"`, `"*"`, or `"+"` functions just as they do in regular expressions. When a group is optional or repeated and it's preceded by a divider then the divider is also optional or repeated. For example, `"/foo/:bar?"` will match `"/foo"`, `"/foo/"`, or `"/foo/baz"`. Escaping the divider will make it required instead.
* A way to greedily match characters, even across dividers, by using `"(.*)"` (so-called unnamed groups).
Currently we plan to have these known differences with path-to-regexp:
* No support for custom prefixes and suffixes.
Canonicalization
===
URLs have a canonical form that is based on ASCII, meaning that [internationalized domain names](https://en.wikipedia.org/wiki/Internationalized_domain_name) (hostnames) also have a canonical ASCII based representation, and that other components such as `hash`, `search` and `pathname` are encoded using [percent encoding](https://en.wikipedia.org/wiki/Percent-encoding).
Currently `URLPattern` does not perform any encoding or normalization of the patterns. So a developer would need to URL encode unicode characters before passing the pattern into the constructor. Similarly, the constructor does not do things like flattening pathnames such as /foo/../bar to /bar. Currently the pattern must be written to target canonical URL output manually.
It does, however, perform these operations for `test()` and `exec()` input.
Encoding components can easily be done manually, but do not encoding the pattern syntax:
```javascript
encodeURIComponent("?q=æøå")
// "%3Fq%3D%C3%A6%C3%B8%C3%A5"
```
```javascript
new URL("https://ølerlækkernårdetermit.dk").hostname
// "xn--lerlkkernrdetermit-dubo78a.dk"
```
Breaking changes
===
- V9.0.0 drops support for NodeJS 14 and lower. NodeJS 15 or higher is required. This is due to using private class fields, so we can have better optimalizations. There is _No_ change in functionality, but we were able to reduce the size of the polyfill by ~2.5KB (~13%), thanks to a pr #118 from @jimmywarting.
Learn more
===
- [Explainer](https://github.com/wanderview/service-worker-scope-pattern-matching/blob/master/explainer.md)
- [Design Document](https://docs.google.com/document/d/17L6b3zlTHtyxQvOAvbK55gQOi5rrJLERwjt_sKXpzqc/edit#)
Reporting a security issue
===
If you have information about a security issue or vulnerability with an Intel-maintained open source project on https://github.com/intel, please send an e-mail to secure@intel.com. Encrypt sensitive information using our PGP public key. For issues related to Intel products, please visit https://security-center.intel.com.
================================================
FILE: index.cjs
================================================
const { URLPattern } = require("./dist/urlpattern.cjs");
module.exports = { URLPattern };
if (!globalThis.URLPattern) {
globalThis.URLPattern = URLPattern;
}
================================================
FILE: index.html
================================================
================================================
FILE: index.js
================================================
import { URLPattern } from "./dist/urlpattern.js";
export { URLPattern };
if (!globalThis.URLPattern) {
globalThis.URLPattern = URLPattern;
}
================================================
FILE: package.json
================================================
{
"name": "urlpattern-polyfill",
"version": "10.0.0",
"description": "Polyfill for the URLPattern API",
"repository": {
"type": "git",
"url": "https://github.com/kenchris/urlpattern-polyfill"
},
"type": "module",
"main": "./index.cjs",
"module": "./index.js",
"types": "./dist/index.d.ts",
"exports": {
"./urlpattern": {
"types": "./dist/index.d.ts",
"import": "./dist/urlpattern.js",
"require": "./dist/urlpattern.cjs"
},
".": {
"types": "./dist/index.d.ts",
"import": "./index.js",
"require": "./index.cjs"
}
},
"tags": [
"url",
"urlpattern",
"url-pattern",
"browser",
"node",
"polyfill",
"w3c",
"wicg"
],
"files": [
"dist",
"index.js",
"index.cjs"
],
"devDependencies": {
"@ava/typescript": "^4.0.0",
"ava": "^5.3.0",
"esbuild": "^0.17.19",
"rimraf": "^5.0.1",
"typescript": "^5.1.3",
"wireit": "^0.9.5"
},
"ava": {
"files": [
"test/**/*",
"!test/wpt"
],
"typescript": {
"rewritePaths": {
"src/": "dist/"
},
"compile": false
}
},
"scripts": {
"build": "wireit",
"sync-wpt": "wireit",
"test": "wireit",
"manual-test": "wireit",
"publish-dev": "wireit",
"publish-patch": "wireit",
"publish-major": "wireit"
},
"wireit": {
"build:esm": {
"command": "esbuild --bundle --format=esm src/url-pattern.ts --outfile=dist/urlpattern.js --minify --keep-names --target=es2022",
"output": [
"dist/urlpattern.js"
],
"files": [
"src/**/*"
]
},
"build:cjs": {
"command": "esbuild --bundle --format=cjs src/url-pattern.ts --outfile=dist/urlpattern.cjs --minify --keep-names --target=es2022",
"output": [
"dist/urlpattern.cjs"
],
"files": [
"src/**/*"
]
},
"copyTypeFiles": {
"command": "cp ./src/index.d.ts ./src/types.d.ts ./dist",
"output": [
"dist/index.d.ts",
"dist/types.d.ts"
],
"dependencies": [
"build:esm",
"build:cjs"
]
},
"build": {
"dependencies": [
"copyTypeFiles"
]
},
"prepFakeNodeModules": {
"command": "rm -rf node_modules/urlpattern-polyfill; ln -s $(pwd) node_modules/urlpattern-polyfill"
},
"sync-wpt": {
"command": "cd test && wget http://wpt.live/urlpattern/resources/urlpatterntestdata.json && wget http://wpt.live/urlpattern/resources/urlpattern-compare-test-data.json"
},
"test": {
"command": "ava --timeout=60s",
"watch": "test/**/*",
"files": [
"test/**/*"
],
"dependencies": [
"prepFakeNodeModules",
"build"
]
},
"manual-test": {
"command": "npx http-server -o /index.html -p 4203",
"dependencies": [
"test"
]
},
"publish-dev": {
"command": "VERSION=${npm_package_version%-*}-dev.`git rev-parse --short HEAD` && npm version --no-git-tag-version $VERSION && npm publish --tag dev",
"dependencies": [
"test"
]
},
"publish-patch": {
"command": "npm version patch && npm publish",
"dependencies": [
"test"
]
},
"publish-major": {
"command": "npm version major && npm publish",
"dependencies": [
"test"
]
}
},
"author": "",
"license": "MIT",
"dependencies": {}
}
================================================
FILE: src/LICENSE.path-to-regex
================================================
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: src/index.d.ts
================================================
import type * as Types from "./types.js";
export { URLPattern } from "./types.js";
declare global {
class URLPattern extends Types.URLPattern {}
type URLPatternInit = Types.URLPatternInit;
type URLPatternResult = Types.URLPatternResult;
type URLPatternComponentResult = Types.URLPatternComponentResult;
}
================================================
FILE: src/parseShorthand.ts
================================================
export function parseShorthand(str: string) {
let protocol = '';
let hostname = '';
let pathname = '';
let search = '';
let hash = '';
let i = str.indexOf('://');
if (i !== -1) {
protocol = str.substring(0, i);
str = str.substring(i + 3);
i = str.indexOf('/');
hostname = str.substring(0, i);
str = str.substring(i + 1);
}
i = str.indexOf('#');
if (i !== -1) {
hash = str.substring(i + 1);
str = str.substring(0, i);
}
str = str
.replace(/(:\w+)\?/g, (_, name) => name + '§')
.replace(/\*\?/g, '*§')
.replace(/\)\?/g, ')§');
i = str.indexOf('?');
if (i !== -1) {
pathname = str.substring(0, i).replace('§', '?');
search = str.substring(i + 1).replace('§', '?');
} else {
pathname = str.replace('§', '?');
}
return { protocol, hostname, pathname, search, hash };
}
================================================
FILE: src/path-to-regex-modified.ts
================================================
/**
* Tokenizer results.
*/
export interface LexToken {
type:
| "OPEN"
| "CLOSE"
| "REGEX"
| "NAME"
| "CHAR"
| "ESCAPED_CHAR"
| "OTHER_MODIFIER"
| "ASTERISK"
| "END"
| "INVALID_CHAR";
index: number;
value: string;
}
export enum Modifier {
// The `*` modifier.
kZeroOrMore = 0,
// The `?` modifier.
kOptional = 1,
// The `+` modifier.
kOneOrMore = 2,
// No modifier.
kNone = 3,
};
export enum PartType {
// A part that matches any character to the end of the input string.
kFullWildcard = 0,
// A part that matches any character to the next segment separator.
kSegmentWildcard = 1,
// A part with a custom regular expression.
kRegex = 2,
// A fixed, non-variable part of the pattern. Consists of kChar and
// kEscapedChar Tokens.
kFixed = 3,
}
export class Part {
type: PartType = PartType.kFixed;
name: string = '';
prefix: string = '';
value: string = '';
suffix: string = '';
modifier: Modifier = Modifier.kNone;
constructor(type: PartType, name: string, prefix: string, value: string, suffix: string, modifier: Modifier) {
this.type = type;
this.name = name;
this.prefix = prefix;
this.value = value;
this.suffix = suffix;
this.modifier = modifier;
}
hasCustomName() {
return this.name !== "" && typeof this.name !== "number";
}
}
// Note, the `//u` suffix triggers this typescript linting bug:
//
// https://github.com/buzinas/tslint-eslint-rules/issues/289
//
// This requires disabling the no-empty-character-class lint rule.
const regexIdentifierStart = /[$_\p{ID_Start}]/u;
const regexIdentifierPart = /[$_\u200C\u200D\p{ID_Continue}]/u;
const kFullWildcardRegex = ".*";
function isASCII(str: string, extended: boolean) {
return (extended ? /^[\x00-\xFF]*$/ : /^[\x00-\x7F]*$/).test(str);
}
/**
* Tokenize input string.
*/
export function lexer(str: string, lenient: boolean = false): LexToken[] {
const tokens: LexToken[] = [];
let i = 0;
while (i < str.length) {
const char = str[i];
const ErrorOrInvalid = function (msg: string) {
if (!lenient) throw new TypeError(msg);
tokens.push({ type: "INVALID_CHAR", index: i, value: str[i++] });
};
if (char === "*") {
tokens.push({ type: "ASTERISK", index: i, value: str[i++] });
continue;
}
if (char === "+" || char === "?") {
tokens.push({ type: "OTHER_MODIFIER", index: i, value: str[i++] });
continue;
}
if (char === "\\") {
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
continue;
}
if (char === "{") {
tokens.push({ type: "OPEN", index: i, value: str[i++] });
continue;
}
if (char === "}") {
tokens.push({ type: "CLOSE", index: i, value: str[i++] });
continue;
}
if (char === ":") {
let name = "";
let j = i + 1;
while (j < str.length) {
const code = str.substr(j, 1);
if (
(j === i + 1 && regexIdentifierStart.test(code)) ||
(j !== i + 1 && regexIdentifierPart.test(code))
) {
name += str[j++];
continue;
}
break;
}
if (!name) {
ErrorOrInvalid(`Missing parameter name at ${i}`);
continue;
}
tokens.push({ type: "NAME", index: i, value: name });
i = j;
continue;
}
if (char === "(") {
let count = 1;
let pattern = "";
let j = i + 1;
let error = false;
if (str[j] === "?") {
ErrorOrInvalid(`Pattern cannot start with "?" at ${j}`);
continue;
}
while (j < str.length) {
if (!isASCII(str[j], false)) {
ErrorOrInvalid(`Invalid character '${str[j]}' at ${j}.`);
error = true;
break;
}
if (str[j] === "\\") {
pattern += str[j++] + str[j++];
continue;
}
if (str[j] === ")") {
count--;
if (count === 0) {
j++;
break;
}
} else if (str[j] === "(") {
count++;
if (str[j + 1] !== "?") {
ErrorOrInvalid(`Capturing groups are not allowed at ${j}`);
error = true;
break;
}
}
pattern += str[j++];
}
if (error) {
continue;
}
if (count) {
ErrorOrInvalid(`Unbalanced pattern at ${i}`);
continue;
}
if (!pattern) {
ErrorOrInvalid(`Missing pattern at ${i}`);
continue;
}
tokens.push({ type: "REGEX", index: i, value: pattern });
i = j;
continue;
}
tokens.push({ type: "CHAR", index: i, value: str[i++] });
}
tokens.push({ type: "END", index: i, value: "" });
return tokens;
}
/**
* Callback type that is invoked for every plain text part of the pattern.
* This is intended to be used to apply URL canonicalization to the pattern
* itself. This is different from the encode callback used to encode group
* values passed to compile, match, etc.
*/
type EncodePartCallback = (value: string) => string;
export interface ParseOptions {
/**
* Set the default delimiter for repeat parameters. (default: `'/'`)
*/
delimiter?: string;
/**
* List of characters to automatically consider prefixes when parsing.
*/
prefixes?: string;
/**
* Encoding callback to apply to each plaintext part of the pattern.
*/
encodePart?: EncodePartCallback;
}
/**
* Parse a string for the raw tokens.
*/
export function parse(str: string, options: ParseOptions = {}): Part[] {
const tokens = lexer(str);
options.delimiter ??= "/#?";
options.prefixes ??= "./";
const segmentWildcardRegex = `[^${escapeString(options.delimiter)}]+?`;
const result: Part[] = [];
let key = 0;
let i = 0;
let path = "";
let nameSet = new Set();
const tryConsume = (type: LexToken["type"]): string | undefined => {
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value;
};
const tryConsumeModifier = (): string | undefined => {
return tryConsume("OTHER_MODIFIER") ?? tryConsume("ASTERISK");
};
const mustConsume = (type: LexToken["type"]): string => {
const value = tryConsume(type);
if (value !== undefined) return value;
const { type: nextType, index } = tokens[i];
throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`);
};
const consumeText = (): string => {
let result = "";
let value: string | undefined;
// tslint:disable-next-line
while ((value = tryConsume("CHAR") ?? tryConsume("ESCAPED_CHAR"))) {
result += value;
}
return result;
};
const DefaultEncodePart = (value: string): string => {
return value;
};
const encodePart = options.encodePart || DefaultEncodePart;
let pendingFixedValue: string = '';
const appendToPendingFixedValue = (value: string) => {
pendingFixedValue += value;
}
const maybeAddPartFromPendingFixedValue = () => {
if (!pendingFixedValue.length) {
return;
}
result.push(new Part(PartType.kFixed, "", "", encodePart(pendingFixedValue), "", Modifier.kNone));
pendingFixedValue = '';
}
const addPart = (prefix: string, nameToken: string, regexOrWildcardToken: string, suffix: string, modifierToken: string) => {
let modifier = Modifier.kNone;
switch (modifierToken) {
case '?':
modifier = Modifier.kOptional;
break;
case '*':
modifier = Modifier.kZeroOrMore;
break;
case '+':
modifier = Modifier.kOneOrMore;
break;
}
// If this is a `{ ... }` grouping containing only fixed text, then
// just add it to our pending value for now. We want to collect as
// much fixed text as possible in the buffer before commiting it to
// a fixed part.
if (!nameToken && !regexOrWildcardToken && modifier === Modifier.kNone) {
appendToPendingFixedValue(prefix);
return;
}
// We are about to add some kind of matching group Part to the list.
// Before doing that make sure to flush any pending fixed test to a
// kFixed Part.
maybeAddPartFromPendingFixedValue();
// If there is no name, regex, or wildcard tokens then this is just a fixed
// string grouping; e.g. "{foo}?". The fixed string ends up in the prefix
// value since it consumed the entire text of the grouping. If the prefix
// value is empty then its an empty "{}" group and we return without adding
// any Part.
if (!nameToken && !regexOrWildcardToken) {
if (!prefix) {
return;
}
result.push(new Part(PartType.kFixed, "", "", encodePart(prefix), "", modifier));
return;
}
// Determine the regex value. If there is a |kRegex| Token, then this is
// explicitly set by that Token. If there is a wildcard token, then this
// is set to the |kFullWildcardRegex| constant. Otherwise a kName Token by
// itself gets an implicit regex value that matches through to the end of
// the segment. This is represented by the |regexOrWildcardToken| value.
let regexValue;
if (!regexOrWildcardToken) {
regexValue = segmentWildcardRegex;
} else if (regexOrWildcardToken === '*') {
regexValue = kFullWildcardRegex;
} else {
regexValue = regexOrWildcardToken;
}
// Next determine the type of the Part. This depends on the regex value
// since we give certain values special treatment with their own type.
// A |segmentWildcardRegex| is mapped to the kSegmentWildcard type. A
// |kFullWildcardRegex| is mapped to the kFullWildcard type. Otherwise
// the Part gets the kRegex type.
let type = PartType.kRegex;
if (regexValue === segmentWildcardRegex) {
type = PartType.kSegmentWildcard;
regexValue = "";
} else if (regexValue === kFullWildcardRegex) {
type = PartType.kFullWildcard;
regexValue = "";
}
// Every kRegex, kSegmentWildcard, and kFullWildcard Part must have a
// group name. If there was a kName Token, then use the explicitly
// set name. Otherwise we generate a numeric based key for the name.
let name;
if (nameToken) {
name = nameToken;
} else if (regexOrWildcardToken) {
name = key++;
}
if (nameSet.has(name)) {
throw new TypeError(`Duplicate name '${name}'.`);
}
nameSet.add(name);
result.push(new Part(type, name, encodePart(prefix), regexValue, encodePart(suffix), modifier));
}
while (i < tokens.length) {
// Look for the sequence:
// There could be from zero to all through of these tokens. For
// example:
// * "/:foo(bar)?" - all four tokens
// * "/" - just a char token
// * ":foo" - just a name token
// * "(bar)" - just a regex token
// * "/:foo" - char and name tokens
// * "/(bar)" - char and regex tokens
// * "/:foo?" - char, name, and modifier tokens
// * "/(bar)?" - char, regex, and modifier tokens
const charToken = tryConsume("CHAR");
const nameToken = tryConsume("NAME");
let regexOrWildcardToken = tryConsume("REGEX");
// If there is no name or regex token, then we may have a wildcard `*`
// token in place of an unnamed regex token. Each wildcard will be
// treated as being equivalent to a "(.*)" regex token. For example:
// * "/*" - equivalent to "/(.*)"
// * "/*?" - equivalent to "/(.*)?"
if (!nameToken && !regexOrWildcardToken) {
regexOrWildcardToken = tryConsume("ASTERISK");
}
// If there is a name, regex, or wildcard token then we need to add a
// Pattern Part immediately.
if (nameToken || regexOrWildcardToken) {
// Determine if the char token is a valid prefix. Only characters in the
// configured prefix_list are automatically treated as prefixes. A
// kEscapedChar Token is never treated as a prefix.
let prefix = charToken ?? "";
if (options.prefixes.indexOf(prefix) === -1) {
// This is not a prefix character. Add it to the buffered characters
// to be added as a kFixed Part later.
appendToPendingFixedValue(prefix);
prefix = "";
}
// If we have any buffered characters in a pending fixed value, then
// convert them into a kFixed Part now.
maybeAddPartFromPendingFixedValue();
// kName and kRegex tokens can optionally be followed by a modifier.
let modifierToken = tryConsumeModifier();
// Add the Part for the name and regex/wildcard tokens.
addPart(prefix, nameToken, regexOrWildcardToken, "", modifierToken);
continue;
}
// There was neither a kRegex or kName token, so consider if we just have a
// fixed string part. A fixed string can consist of kChar or kEscapedChar
// tokens. These just get added to the buffered pending fixed value for
// now. It will get converted to a kFixed Part later.
const value = charToken ?? tryConsume("ESCAPED_CHAR");
if (value) {
appendToPendingFixedValue(value);
continue;
}
// There was not a char or escaped char token, so we no we are at the end
// of any fixed string. Do not yet convert the pending fixed value into
// a kFixedPart, though. Its possible there will be further fixed text in
// a `{ ... }` group, etc.
// Look for the sequence:
//
//
//
// The open and close are required, but the other tokens are optional.
// For example:
// * "{a:foo(.*)b}?" - all tokens present
// * "{:foo}?" - just name and modifier tokens
// * "{(.*)}?" - just regex and modifier tokens
// * "{ab}?" - just char and modifier tokens
const openToken = tryConsume("OPEN");
if (openToken) {
const prefix = consumeText();
const nameToken = tryConsume("NAME");
let regexOrWildcardToken = tryConsume("REGEX");
// If there is no name or regex token, then we may have a wildcard `*`
// token in place of an unnamed regex token. Each wildcard will be
// treated as being equivalent to a "(.*)" regex token. For example,
// "{a*b}" is equivalent to "{a(.*)b}".
if (!nameToken && !regexOrWildcardToken) {
regexOrWildcardToken = tryConsume("ASTERISK");
}
const suffix = consumeText();
mustConsume("CLOSE");
const modifierToken = tryConsumeModifier();
addPart(prefix, nameToken, regexOrWildcardToken, suffix, modifierToken);
continue;
}
// We are about to end the pattern string, so flush any pending text to
// a kFixed Part.
maybeAddPartFromPendingFixedValue();
// We didn't find any tokens allowed by the syntax, so we should be
// at the end of the token list. If there is a syntax error, this
// is where it will typically be caught.
mustConsume("END");
}
return result;
}
/**
* Escape a regular expression string.
*/
function escapeString(str: string) {
return str.replace(/([.+*?^${}()[\]|/\\])/g, "\\$1");
}
/**
* Get the flags for a regexp from the options.
*/
function flags(options?: { ignoreCase?: boolean }) {
return options && options.ignoreCase ? "ui" : "u";
}
/**
* Create a path regexp from string input.
*/
export function stringToRegexp(
path: string,
names?: string[],
options?: Options & ParseOptions
) {
return partsToRegexp(parse(path, options), names, options);
}
export interface Options {
/**
* When `true` the regexp will be case insensitive. (default: `false`)
*/
ignoreCase?: boolean;
/**
* When `true` the regexp won't allow an optional trailing delimiter to match. (default: `false`)
*/
strict?: boolean;
/**
* When `true` the regexp will match to the end of the string. (default: `true`)
*/
end?: boolean;
/**
* When `true` the regexp will match from the beginning of the string. (default: `true`)
*/
start?: boolean;
/**
* Sets the final character for non-ending optimistic matches. (default: `/`)
*/
delimiter?: string;
/**
* List of characters that can also be "end" characters.
*/
endsWith?: string;
/**
* Encode path tokens for use in the `RegExp`.
*/
encode?: (value: string) => string;
}
export function modifierToString(modifier: Modifier) {
switch (modifier) {
case Modifier.kZeroOrMore:
return '*';
case Modifier.kOptional:
return '?';
case Modifier.kOneOrMore:
return '+';
case Modifier.kNone:
return '';
}
}
/**
* Expose a function for taking tokens and returning a RegExp.
*/
export function partsToRegexp(
parts: Part[],
names?: string[],
options: Options = {}
) {
options.delimiter ??= "/#?";
options.prefixes ??= "./";
options.sensitive ??= false;
options.strict ??= false;
options.end ??= true;
options.start ??= true;
options.endsWith = '';
let result = options.start ? "^" : "";
// Iterate over the parts and create our regexp string.
for (const part of parts) {
// Handle kFixed Parts. If there is a modifier we must wrap the escaped
// value in a non-capturing group. Otherwise we just append the escaped
// value. For example:
//
//
//
// Or:
//
// (?:)
//
if (part.type === PartType.kFixed) {
if (part.modifier === Modifier.kNone) {
result += escapeString(part.value);
} else {
result += `(?:${escapeString(part.value)})${modifierToString(part.modifier)}`;
}
continue;
}
// All remaining Part types must have a name. Append it to the output
// names if provided.
if (names) names.push(part.name);
const segmentWildcardRegex = `[^${escapeString(options.delimiter)}]+?`;
// Compute the Part regex value. For kSegmentWildcard and kFullWildcard
// types we must convert the type enum back to the defined regex value.
let regexValue = part.value;
if (part.type === PartType.kSegmentWildcard)
regexValue = segmentWildcardRegex;
else if (part.type === PartType.kFullWildcard)
regexValue = kFullWildcardRegex;
// Handle the case where there is no prefix or suffix value. This varies a
// bit depending on the modifier.
//
// If there is no modifier or an optional modifier, then we simply wrap the
// regex value in a capturing group:
//
// ()
//
// If there is a modifier, then we need to use a non-capturing group for the
// regex value and an outer capturing group that includes the modifier as
// well. Like:
//
// ((?:))
if (!part.prefix.length && !part.suffix.length) {
if (part.modifier === Modifier.kNone ||
part.modifier === Modifier.kOptional) {
result += `(${regexValue})${modifierToString(part.modifier)}`;
} else {
result += `((?:${regexValue})${modifierToString(part.modifier)})`;
}
continue;
}
// Handle non-repeating regex Parts with a prefix and/or suffix. The
// capturing group again only contains the regex value. This inner group
// is compined with the prefix and/or suffix in an outer non-capturing
// group. Finally the modifier is applied to the entire outer group.
// For example:
//
// (?:())
//
if (part.modifier === Modifier.kNone ||
part.modifier === Modifier.kOptional) {
result += `(?:${escapeString(part.prefix)}(${regexValue})${escapeString(part.suffix)})`;
result += modifierToString(part.modifier);
continue;
}
// Repeating Parts are dramatically more complicated. We want to exclude
// the initial prefix and the final suffix, but include them between any
// repeated elements. To achieve this we provide a separate initial
// part that excludes the prefix. Then the part is duplicated with the
// prefix/suffix values included in an optional repeating element. If
// zero values are permitted then a final optional modifier may be added.
// For example:
//
// (?:((?:)(?:(?:))*))?
//
result += `(?:${escapeString(part.prefix)}`;
result += `((?:${regexValue})(?:`;
result += escapeString(part.suffix);
result += escapeString(part.prefix);
result += `(?:${regexValue}))*)${escapeString(part.suffix)})`;
if (part.modifier === Modifier.kZeroOrMore) {
result += "?";
}
}
const endsWith = `[${escapeString(options.endsWith)}]|$`;
const delimiter = `[${escapeString(options.delimiter)}]`;
// Should we anchor the pattern to the end of the input string?
if (options.end) {
// In non-strict mode an optional delimiter character is always
// permitted at the end of the string. For example, if the pattern
// is "/foo/bar" then it would match "/foo/bar/".
//
// []?
//
if (!options.strict) {
result += `${delimiter}?`;
}
// The options ends_with value contains a list of characters that
// may also signal the end of the pattern match.
if (!options.endsWith.length) {
// Simply anchor to the end of the input string.
result += "$";
} else {
// Anchor to either a ends_with character or the end of the input
// string. This uses a lookahead assertion.
//
// (?=[]|$)
//
result += `(?=${endsWith})`;
}
return new RegExp(result, flags(options));
}
// We are not anchored to the end of the input string.
// Again, if not in strict mode we permit an optional trailing delimiter
// character before anchoring to any ends_with characters with a lookahead
// assertion.
//
// (?:[](?=[]|$))?
if (!options.strict) {
result += `(?:${delimiter}(?=${endsWith}))?`;
}
// Further, if the pattern does not end with a trailing delimiter character
// we also anchor to a delimiter character in our lookahead assertion. So
// a pattern "/foo/bar" would match "/foo/bar/baz", but not "/foo/barbaz".
//
// (?=[]|[]|$)
let isEndDelimited = false;
if (parts.length) {
const lastPart = parts[parts.length - 1];
if (lastPart.type === PartType.kFixed && lastPart.modifier === Modifier.kNone) {
isEndDelimited = options.delimiter.indexOf(lastPart) > -1;
}
}
if (!isEndDelimited) {
result += `(?=${delimiter}|${endsWith})`;
}
return new RegExp(result, flags(options));
}
================================================
FILE: src/types.d.ts
================================================
export type URLPatternInput = URLPatternInit | string;
export declare class URLPattern {
constructor(init?: URLPatternInput, baseURL?: string);
test(input?: URLPatternInput, baseURL?: string): boolean;
exec(input?: URLPatternInput, baseURL?: string): URLPatternResult | null;
readonly protocol: string;
readonly username: string;
readonly password: string;
readonly hostname: string;
readonly port: string;
readonly pathname: string;
readonly search: string;
readonly hash: string;
}
interface URLPatternInit {
baseURL?: string;
username?: string;
password?: string;
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
hash?: string;
}
export interface URLPatternResult {
inputs: [URLPatternInput];
protocol: URLPatternComponentResult;
username: URLPatternComponentResult;
password: URLPatternComponentResult;
hostname: URLPatternComponentResult;
port: URLPatternComponentResult;
pathname: URLPatternComponentResult;
search: URLPatternComponentResult;
hash: URLPatternComponentResult;
}
export interface URLPatternComponentResult {
input: string;
groups: {
[key: string]: string | undefined;
};
}
================================================
FILE: src/url-pattern-list.ts
================================================
import { parseShorthand } from './parseShorthand';
import { URLPattern } from './url-pattern';
import { URLPatternResult } from './url-pattern.interfaces';
export class URLPatternList {
private patterns: Array = [];
constructor(list: URLPattern[], options = {}) {
if (!Array.isArray(list)) {
throw TypeError('parameter list must be if type URLPattern[]');
}
const firstItem = list[0];
if (firstItem instanceof URLPattern) {
for (let pattern of list) {
if (!(pattern instanceof URLPattern)) {
throw TypeError('parameter list must be if type URLPattern[]');
}
this.patterns.push(pattern);
}
} else {
try {
for (let patternInit of list) {
let init = {};
if (typeof patternInit === 'object') {
init = Object.assign(Object.assign({}, options), patternInit);
} else if (typeof patternInit === 'string') {
init = Object.assign(Object.assign({}, options), parseShorthand(patternInit));
} else {
throw new TypeError('List contains no parsable information');
}
this.patterns.push(new URLPattern(init));
}
} catch {
throw new TypeError('List contains no parsable information');
}
}
}
test(url: string) {
try {
new URL(url); // allows string or URL object.
} catch {
return false;
}
for (let urlPattern of this.patterns) {
if (urlPattern.test(url)) {
return true;
}
}
return false;
}
exec(url: string): URLPatternResult | null | number {
try {
new URL(url); // allows string or URL object.
} catch {
return null;
}
for (let urlPattern of this.patterns) {
const value = urlPattern.exec(url);
if (value) {
return value;
}
}
return null;
}
}
================================================
FILE: src/url-pattern-parser.ts
================================================
// The parse has been translated from the chromium c++ implementation at:
//
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/url_pattern/url_pattern_parser.h;l=36;drc=f66c35e3c41629675130001dbce0dcfba870160b
//
import {lexer, LexToken, stringToRegexp, ParseOptions, Options} from './path-to-regex-modified';
import {URLPatternInit} from './url-pattern.interfaces';
import {DEFAULT_OPTIONS, protocolEncodeCallback, isSpecialScheme} from './url-utils';
enum State {
INIT,
PROTOCOL,
AUTHORITY,
USERNAME,
PASSWORD,
HOSTNAME,
PORT,
PATHNAME,
SEARCH,
HASH,
DONE,
}
// A helper class to parse the first string passed to the URLPattern
// constructor. In general the parser works by using the path-to-regexp
// lexer to first split up the input into pattern tokens. It can
// then look through the tokens to find non-special characters that match
// the different URL component separators. Each component is then split
// off and stored in a `URLPatternInit` object that can be accessed via
// the `Parser.result` getter. The intent is that this init object should
// then be processed as if it was passed into the constructor itself.
export class Parser {
// The input string to the parser.
#input: string;
// The list of `LexToken`s produced by the path-to-regexp `lexer()` function
// when passed `input` with lenient mode enabled.
#tokenList: LexToken[] = [];
// As we parse the input string we populate a `URLPatternInit` dictionary
// with each component pattern. This is then the final result of the parse.
#internalResult: URLPatternInit = {};
// The index of the current `LexToken` being considered.
#tokenIndex: number = 0;
// The value to add to `tokenIndex` on each turn through the parse loop.
// While typically this is `1`, it is also set to `0` at times for things
// like state transitions, etc. It is automatically reset back to `1` at
// the top of the parse loop.
#tokenIncrement: number = 1;
// The index of the first `LexToken` to include in the component string.
#componentStart: number = 0;
// The current parse state. This should only be changed via `changeState()`
// or `rewindAndSetState()`.
#state: State = State.INIT;
// The current nest depth of `{ }` pattern groupings.
#groupDepth: number = 0;
// The current nesting depth of `[ ]` in hostname patterns.
#hostnameIPv6BracketDepth: number = 0;
// True if we should apply parse rules as if this is a "standard" URL. If
// false then this is treated as a "not a base URL".
#shouldTreatAsStandardURL: boolean = false;
public constructor(input: string) {
this.#input = input;
}
// Return the parse result. The result is only available after the
// `parse()` method completes.
public get result(): URLPatternInit {
return this.#internalResult;
}
// Attempt to parse the input string used to construct the Parser object.
// This method may only be called once. Any errors will be thrown as an
// exception. Retrieve the parse result by accessing the `Parser.result`
// property getter.
public parse(): void {
this.#tokenList = lexer(this.#input, /*lenient=*/true);
for (; this.#tokenIndex < this.#tokenList.length;
this.#tokenIndex += this.#tokenIncrement) {
// Reset back to our default tokenIncrement value.
this.#tokenIncrement = 1;
// All states must respect the end of the token list. The path-to-regexp
// lexer guarantees that the last token will have the type `END`.
if (this.#tokenList[this.#tokenIndex].type === 'END') {
// If we failed to find a protocol terminator then we are still in
// relative mode. We now need to determine the first component of the
// relative URL.
if (this.#state === State.INIT) {
// Reset back to the start of the input string.
this.#rewind();
// If the string begins with `?` then its a relative search component.
// If it starts with `#` then its a relative hash component. Otherwise
// its a relative pathname.
if (this.#isHashPrefix()) {
this.#changeState(State.HASH, /*skip=*/1);
} else if (this.#isSearchPrefix()) {
this.#changeState(State.SEARCH, /*skip=*/1);
} else {
this.#changeState(State.PATHNAME, /*skip=*/0);
}
continue;
}
//
// If we failed to find an `@`, then there is no username and password.
// We should rewind and process the data as a hostname.
else if (this.#state === State.AUTHORITY) {
this.#rewindAndSetState(State.HOSTNAME);
continue;
}
this.#changeState(State.DONE, /*skip=*/0);
break;
}
// In addition, all states must handle pattern groups. We do not permit
// a component to end in the middle of a pattern group. Therefore we skip
// past any tokens that are within `{` and `}`. Note, the tokenizer
// handles group `(` and `)` and `:foo` groups for us automatically, so
// we don't need special code for them here.
if (this.#groupDepth > 0) {
if (this.#isGroupClose()) {
this.#groupDepth -= 1;
} else {
continue;
}
}
if (this.#isGroupOpen()) {
this.#groupDepth += 1;
continue;
}
switch (this.#state) {
case State.INIT:
if (this.#isProtocolSuffix()) {
// Update the state to expect the start of an absolute URL.
this.#rewindAndSetState(State.PROTOCOL);
}
break;
case State.PROTOCOL:
// If we find the end of the protocol component...
if (this.#isProtocolSuffix()) {
// First we eagerly compile the protocol pattern and use it to
// compute if this entire URLPattern should be treated as a
// "standard" URL. If any of the special schemes, like `https`,
// match the protocol pattern then we treat it as standard.
this.#computeShouldTreatAsStandardURL();
// By default we treat this as a "cannot-be-a-base-URL" or what chrome
// calls a "path" URL. In this case we go straight to the pathname
// component. The hostname and port are left with their default
// empty string values.
let nextState: State = State.PATHNAME;
let skip: number = 1;
// If there are authority slashes, like `https://`, then
// we must transition to the authority section of the URLPattern.
if (this.#nextIsAuthoritySlashes()) {
nextState = State.AUTHORITY;
skip = 3;
}
// If there are no authority slashes, but the protocol is special
// then we still go to the authority section as this is a "standard"
// URL. This differs from the above case since we don't need to skip
// the extra slashes.
else if (this.#shouldTreatAsStandardURL) {
nextState = State.AUTHORITY;
}
this.#changeState(nextState, skip);
}
break;
case State.AUTHORITY:
// Before going to the hostname state we must see if there is an
// identity of the form:
//
// :@
//
// We check for this by looking for the `@` character. The username
// and password are themselves each optional, so the `:` may not be
// present. If we see the `@` we just go to the username state
// and let it proceed until it hits either the password separator
// or the `@` terminator.
if (this.#isIdentityTerminator()) {
this.#rewindAndSetState(State.USERNAME);
}
// Stop searching for the `@` character if we see the beginning
// of the pathname, search, or hash components.
else if (this.#isPathnameStart() || this.#isSearchPrefix() ||
this.#isHashPrefix()) {
this.#rewindAndSetState(State.HOSTNAME);
}
break;
case State.USERNAME:
// If we find a `:` then transition to the password component state.
if (this.#isPasswordPrefix()) {
this.#changeState(State.PASSWORD, /*skip=*/1);
}
// If we find a `@` then transition to the hostname component state.
else if (this.#isIdentityTerminator()) {
this.#changeState(State.HOSTNAME, /*skip=*/1);
}
break;
case State.PASSWORD:
// If we find a `@` then transition to the hostname component state.
if (this.#isIdentityTerminator()) {
this.#changeState(State.HOSTNAME, /*skip=*/1);
}
break;
case State.HOSTNAME:
// Track whether we are inside ipv6 address brackets.
if (this.#isIPv6Open()) {
this.#hostnameIPv6BracketDepth += 1;
} else if (this.#isIPv6Close()) {
this.#hostnameIPv6BracketDepth -= 1;
}
// If we find a `:` then we transition to the port component state.
// However, we ignore `:` when parsing an ipv6 address.
if (this.#isPortPrefix() && !this.#hostnameIPv6BracketDepth) {
this.#changeState(State.PORT, /*skip=*/1);
}
// If we find a `/` then we transition to the pathname component state.
else if (this.#isPathnameStart()) {
this.#changeState(State.PATHNAME, /*skip=*/0);
}
// If we find a `?` then we transition to the search component state.
else if (this.#isSearchPrefix()) {
this.#changeState(State.SEARCH, /*skip=*/1);
}
// If we find a `#` then we transition to the hash component state.
else if (this.#isHashPrefix()) {
this.#changeState(State.HASH, /*skip=*/1);
}
break;
case State.PORT:
// If we find a `/` then we transition to the pathname component state.
if (this.#isPathnameStart()) {
this.#changeState(State.PATHNAME, /*skip=*/0);
}
// If we find a `?` then we transition to the search component state.
else if (this.#isSearchPrefix()) {
this.#changeState(State.SEARCH, /*skip=*/1);
}
// If we find a `#` then we transition to the hash component state.
else if (this.#isHashPrefix()) {
this.#changeState(State.HASH, /*skip=*/1);
}
break;
case State.PATHNAME:
// If we find a `?` then we transition to the search component state.
if (this.#isSearchPrefix()) {
this.#changeState(State.SEARCH, /*skip=*/1);
}
// If we find a `#` then we transition to the hash component state.
else if (this.#isHashPrefix()) {
this.#changeState(State.HASH, /*skip=*/1);
}
break;
case State.SEARCH:
// If we find a `#` then we transition to the hash component state.
if (this.#isHashPrefix()) {
this.#changeState(State.HASH, /*skip=*/1);
}
break;
case State.HASH:
// Nothing to do here as we are just looking for the end.
break;
case State.DONE:
// This should not be reached.
break;
}
}
if (this.#internalResult.hostname !== undefined &&
this.#internalResult.port === undefined) {
// If the hostname is specified in a constructor string but the port is
// not, the default port is assumed to be meant.
this.#internalResult.port = '';
}
}
#changeState(newState: State, skip: number): void {
switch (this.#state) {
case State.INIT:
// No component to set when transitioning from this state.
break;
case State.PROTOCOL:
this.#internalResult.protocol = this.#makeComponentString();
break;
case State.AUTHORITY:
// No component to set when transitioning from this state.
break;
case State.USERNAME:
this.#internalResult.username = this.#makeComponentString();
break;
case State.PASSWORD:
this.#internalResult.password = this.#makeComponentString();
break;
case State.HOSTNAME:
this.#internalResult.hostname = this.#makeComponentString();
break;
case State.PORT:
this.#internalResult.port = this.#makeComponentString();
break;
case State.PATHNAME:
this.#internalResult.pathname = this.#makeComponentString();
break;
case State.SEARCH:
this.#internalResult.search = this.#makeComponentString();
break;
case State.HASH:
this.#internalResult.hash = this.#makeComponentString();
break;
case State.DONE:
// No component to set when transitioning from this state.
break;
}
if (this.#state !== State.INIT && newState !== State.DONE) {
// If hostname, pathname or search is skipped but something appears after
// it, then it takes its default value (usually the empty string).
if ([State.PROTOCOL, State.AUTHORITY, State.USERNAME, State.PASSWORD]
.includes(this.#state) &&
[State.PORT, State.PATHNAME, State.SEARCH, State.HASH]
.includes(newState)) {
this.#internalResult.hostname ??= '';
}
if ([State.PROTOCOL, State.AUTHORITY, State.USERNAME, State.PASSWORD,
State.HOSTNAME, State.PORT]
.includes(this.#state) &&
[State.SEARCH, State.HASH]
.includes(newState)) {
this.#internalResult.pathname ??=
(this.#shouldTreatAsStandardURL ? '/' : '');
}
if ([State.PROTOCOL, State.AUTHORITY, State.USERNAME, State.PASSWORD,
State.HOSTNAME, State.PORT, State.PATHNAME]
.includes(this.#state) &&
newState === State.HASH) {
this.#internalResult.search ??= '';
}
}
this.#changeStateWithoutSettingComponent(newState, skip);
}
#changeStateWithoutSettingComponent(newState: State, skip: number): void {
this.#state = newState;
// Now update `componentStart` to point to the new component. The `skip`
// argument tells us how many tokens to ignore to get to the next start.
this.#componentStart = this.#tokenIndex + skip;
// Next, move the `tokenIndex` so that the top of the loop will begin
// parsing the new component.
this.#tokenIndex += skip;
this.#tokenIncrement = 0;
}
#rewind(): void {
this.#tokenIndex = this.#componentStart;
this.#tokenIncrement = 0;
}
#rewindAndSetState(newState: State): void {
this.#rewind();
this.#state = newState;
}
#safeToken(index: number): LexToken {
if (index < 0) {
index = this.#tokenList.length - index;
}
if (index < this.#tokenList.length) {
return this.#tokenList[index];
}
return this.#tokenList[this.#tokenList.length - 1];
}
#isNonSpecialPatternChar(index: number, value: string): boolean {
const token: LexToken = this.#safeToken(index);
return token.value === value &&
(token.type === 'CHAR' ||
token.type === 'ESCAPED_CHAR' ||
token.type === 'INVALID_CHAR');
}
#isProtocolSuffix(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, ':');
}
#nextIsAuthoritySlashes(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex + 1, '/') &&
this.#isNonSpecialPatternChar(this.#tokenIndex + 2, '/');
}
#isIdentityTerminator(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, '@');
}
#isPasswordPrefix(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, ':');
}
#isPortPrefix(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, ':');
}
#isPathnameStart(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, '/');
}
#isSearchPrefix(): boolean {
if (this.#isNonSpecialPatternChar(this.#tokenIndex, '?')) {
return true;
}
if (this.#tokenList[this.#tokenIndex].value !== '?') {
return false;
}
// We have a `?` tokenized as a modifier. We only want to treat this as
// the search prefix if it would not normally be valid in a path-to-regexp
// string. A modifier must follow a matching group. Therefore we inspect
// the preceding token to if the `?` is immediately following a group
// construct.
//
// So if the string is:
//
// https://exmaple.com/foo?bar
//
// Then we return true because the previous token is a `o` with type `CHAR`.
// For the string:
//
// https://example.com/:name?bar
//
// Then we return false because the previous token is `:name` with type
// `NAME`. If the developer intended this to be a search prefix then they
// would need to escape the quest mark like `:name\\?bar`.
//
// Note, if `tokenIndex` is zero the index will wrap around and
// `safeToken()` will return the `END` token. This will correctly return
// true from this method as a pattern cannot normally begin with an
// unescaped `?`.
const previousToken: LexToken = this.#safeToken(this.#tokenIndex - 1);
return previousToken.type !== 'NAME' &&
previousToken.type !== 'REGEX' &&
previousToken.type !== 'CLOSE' &&
previousToken.type !== 'ASTERISK';
}
#isHashPrefix(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, '#');
}
#isGroupOpen(): boolean {
return this.#tokenList[this.#tokenIndex].type == 'OPEN';
}
#isGroupClose(): boolean {
return this.#tokenList[this.#tokenIndex].type == 'CLOSE';
}
#isIPv6Open(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, '[');
}
#isIPv6Close(): boolean {
return this.#isNonSpecialPatternChar(this.#tokenIndex, ']');
}
#makeComponentString(): string {
const token: LexToken = this.#tokenList[this.#tokenIndex];
const componentCharStart = this.#safeToken(this.#componentStart).index;
return this.#input.substring(componentCharStart, token.index);
}
#computeShouldTreatAsStandardURL(): void {
const options: Options & ParseOptions = {};
Object.assign(options, DEFAULT_OPTIONS);
options.encodePart = protocolEncodeCallback;
const regexp = stringToRegexp(this.#makeComponentString(), /*keys=*/undefined, options);
this.#shouldTreatAsStandardURL = isSpecialScheme(regexp);
}
}
================================================
FILE: src/url-pattern.interfaces.ts
================================================
export interface URLPatternInit {
baseURL?: string;
username?: string;
password?: string;
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
hash?: string;
}
type URLPatternComponent = 'protocol' | 'username' | 'password'
| 'hostname' | 'port' | 'pathname' | 'search' | 'hash';
export type URLPatternKeys = Exclude
export interface URLPatternResult {
inputs: [URLPatternInit | string];
protocol: URLPatternComponentResult;
username: URLPatternComponentResult;
password: URLPatternComponentResult;
hostname: URLPatternComponentResult;
port: URLPatternComponentResult;
pathname: URLPatternComponentResult;
search: URLPatternComponentResult;
hash: URLPatternComponentResult;
}
export interface URLPatternComponentResult {
input: string;
groups: { [key: string]: string };
}
export interface URLPatternOptions {
ignoreCase: boolean;
}
================================================
FILE: src/url-pattern.ts
================================================
import { ParseOptions, Options, parse, Part, PartType, partsToRegexp, Modifier, modifierToString } from './path-to-regex-modified';
import { URLPatternResult, URLPatternInit, URLPatternKeys, URLPatternOptions, URLPatternComponent } from './url-pattern.interfaces';
import {
DEFAULT_OPTIONS,
HOSTNAME_OPTIONS,
PATHNAME_OPTIONS,
canonicalizeHash,
canonicalizeHostname,
canonicalizePassword,
canonicalizePathname,
canonicalizePort,
canonicalizeProtocol,
canonicalizeSearch,
canonicalizeUsername,
defaultPortForProtocol,
treatAsIPv6Hostname,
isAbsolutePathname,
isSpecialScheme,
protocolEncodeCallback,
usernameEncodeCallback,
passwordEncodeCallback,
hostnameEncodeCallback,
ipv6HostnameEncodeCallback,
portEncodeCallback,
standardURLPathnameEncodeCallback,
pathURLPathnameEncodeCallback,
searchEncodeCallback,
hashEncodeCallback,
} from './url-utils';
import { Parser } from './url-pattern-parser';
// Define the components in a URL. The ordering of this constant list is
// signficant to the implementation below.
const COMPONENTS: URLPatternKeys[] = [
'protocol',
'username',
'password',
'hostname',
'port',
'pathname',
'search',
'hash',
];
// The default wildcard pattern used for a component when the constructor
// input does not provide an explicit value.
const DEFAULT_PATTERN = '*';
function extractValues(url: string, baseURL?: string): URLPatternInit {
if (typeof url !== "string") {
throw new TypeError(`parameter 1 is not of type 'string'.`);
}
const o = new URL(url, baseURL); // May throw.
return {
protocol: o.protocol.substring(0, o.protocol.length - 1),
username: o.username,
password: o.password,
hostname: o.hostname,
port: o.port,
pathname: o.pathname,
search: o.search !== '' ? o.search.substring(1, o.search.length) : undefined,
hash: o.hash !== '' ? o.hash.substring(1, o.hash.length) : undefined,
};
}
function processBaseURLString(input: string, isPattern: boolean) {
if (!isPattern) {
return input;
}
return escapePatternString(input);
}
// A utility method that takes a URLPatternInit, splits it apart, and applies
// the individual component values in the given set of strings. The strings
// are only applied if a value is present in the init structure.
function applyInit(o: URLPatternInit, init: URLPatternInit, isPattern: boolean): URLPatternInit {
// If there is a baseURL we need to apply its component values first. The
// rest of the URLPatternInit structure will then later override these
// values.
let baseURL;
if (typeof init.baseURL === 'string') {
try {
baseURL = new URL(init.baseURL);
if (init.protocol === undefined) {
o.protocol = processBaseURLString(baseURL.protocol.substring(0, baseURL.protocol.length - 1), isPattern);
}
if (!isPattern && init.protocol === undefined && init.hostname === undefined &&
init.port === undefined && init.username === undefined) {
o.username = processBaseURLString(baseURL.username, isPattern);
}
if (!isPattern && init.protocol === undefined && init.hostname === undefined &&
init.port === undefined && init.username === undefined &&
init.password === undefined) {
o.password = processBaseURLString(baseURL.password, isPattern);
}
if (init.protocol === undefined && init.hostname === undefined) {
o.hostname = processBaseURLString(baseURL.hostname, isPattern);
}
if (init.protocol === undefined && init.hostname === undefined &&
init.port === undefined) {
o.port = processBaseURLString(baseURL.port, isPattern);
}
if (init.protocol === undefined && init.hostname === undefined &&
init.port === undefined && init.pathname === undefined) {
o.pathname = processBaseURLString(baseURL.pathname, isPattern);
}
if (init.protocol === undefined && init.hostname === undefined &&
init.port === undefined && init.pathname === undefined &&
init.search === undefined) {
o.search = processBaseURLString(baseURL.search.substring(1, baseURL.search.length), isPattern);
}
if (init.protocol === undefined && init.hostname === undefined &&
init.port === undefined && init.pathname === undefined &&
init.search === undefined && init.hash === undefined) {
o.hash = processBaseURLString(baseURL.hash.substring(1, baseURL.hash.length), isPattern);
}
} catch {
throw new TypeError(`invalid baseURL '${init.baseURL}'.`);
}
}
// Apply the URLPatternInit component values on top of the default and
// baseURL values.
if (typeof init.protocol === 'string') {
o.protocol = canonicalizeProtocol(init.protocol, isPattern);
}
if (typeof init.username === 'string') {
o.username = canonicalizeUsername(init.username, isPattern);
}
if (typeof init.password === 'string') {
o.password = canonicalizePassword(init.password, isPattern);
}
if (typeof init.hostname === 'string') {
o.hostname = canonicalizeHostname(init.hostname, isPattern);
}
if (typeof init.port === 'string') {
o.port = canonicalizePort(init.port, o.protocol, isPattern);
}
if (typeof init.pathname === 'string') {
o.pathname = init.pathname;
if (baseURL && !isAbsolutePathname(o.pathname, isPattern)) {
// Find the last slash in the baseURL pathname. Since the URL is
// hierarchical it should have a slash to be valid, but we are cautious
// and check. If there is no slash then we cannot use resolve the
// relative pathname and just treat the init pathname as an absolute
// value.
const slashIndex = baseURL.pathname.lastIndexOf('/');
if (slashIndex >= 0) {
// Extract the baseURL path up to and including the first slash.
// Append the relative init pathname to it.
o.pathname = processBaseURLString(baseURL.pathname.substring(0, slashIndex + 1), isPattern) + o.pathname;
}
}
o.pathname = canonicalizePathname(o.pathname, o.protocol, isPattern);
}
if (typeof init.search === 'string') {
o.search = canonicalizeSearch(init.search, isPattern);
}
if (typeof init.hash === 'string') {
o.hash = canonicalizeHash(init.hash, isPattern);
}
return o;
}
function escapePatternString(value: string): string {
return value.replace(/([+*?:{}()\\])/g, '\\$1');
}
function escapeRegexpString(value: string): string {
return value.replace(/([.+*?^${}()[\]|/\\])/g, '\\$1');
}
// A utility function to convert a list of path-to-regexp Tokens back into
// a pattern string. The resulting pattern should be equivalent to the
// original parsed pattern, although they may differ due to canonicalization.
function partsToPattern(parts: Part[], options: Options & ParseOptions): string {
options.delimiter ??= "/#?";
options.prefixes ??= "./";
options.sensitive ??= false;
options.strict ??= false;
options.end ??= true;
options.start ??= true;
options.endsWith = '';
const kFullWildcardRegex = ".*";
const segmentWildcardRegex = `[^${escapeRegexpString(options.delimiter)}]+?`;
const regexIdentifierPart = /[$_\u200C\u200D\p{ID_Continue}]/u;
let result = "";
for (let i = 0; i < parts.length; ++i) {
const part = parts[i];
if (part.type === PartType.kFixed) {
// A simple fixed string part.
if (part.modifier === Modifier.kNone) {
result += escapePatternString(part.value);
continue;
}
// A fixed string, but with a modifier which requires a grouping.
// For example, `{foo}?`.
result += `{${escapePatternString(part.value)}}${modifierToString(part.modifier)}`;
continue;
}
// Determine if the token name was custom or automatically assigned.
const customName = part.hasCustomName();
// Determine if the token needs a grouping like `{ ... }`. This is
// necessary when the group:
//
// 1. is using a non-automatic prefix or any suffix.
let needsGrouping =
!!part.suffix.length ||
(!!part.prefix.length &&
(part.prefix.length !== 1 ||
!options.prefixes.includes(part.prefix)));
const lastPart = i > 0 ? parts[i - 1] : null;
const nextPart: any = i < parts.length - 1 ? parts[i + 1] : null;
// 2. following by a matching group that may be expressed in a way that can
// be mistakenly interpreted as part of the matching group. For
// example:
//
// a. An `(...)` expression following a `:foo` group. We want to output
// `{:foo}(...)` and not `:foo(...)`.
// b. A plain text expression following a `:foo` group where the text
// could be mistakenly interpreted as part of the name. We want to
// output `{:foo}bar` and not `:foobar`.
if (!needsGrouping && customName &&
part.type === PartType.kSegmentWildcard &&
part.modifier === Modifier.kNone && nextPart && !nextPart.prefix.length &&
!nextPart.suffix.length) {
if (nextPart.type === PartType.kFixed) {
const code = nextPart.value.length > 0 ? nextPart.value[0] : "";
needsGrouping = regexIdentifierPart.test(code);
} else {
needsGrouping = !nextPart.hasCustomName();
}
}
// 3. preceded by a fixed text part that ends with an implicit prefix
// character (like `/`). This occurs when the original pattern used
// an escape or grouping to prevent the implicit prefix; e.g.
// `\\/*` or `/{*}`. In these cases we use a grouping to prevent the
// implicit prefix in the generated string.
if (!needsGrouping && !part.prefix.length && lastPart &&
lastPart.type === PartType.kFixed) {
const code = lastPart.value[lastPart.value.length - 1];
needsGrouping = options.prefixes.includes(code);
}
// This is a full featured token. We must generate a string that looks
// like:
//
// { }
//
// Where the { and } may not be needed. The will be a regexp,
// named group, or wildcard.
if (needsGrouping) {
result += '{';
}
result += escapePatternString(part.prefix);
if (customName) {
result += `:${part.name}`;
}
if (part.type === PartType.kRegex) {
result += `(${part.value})`;
} else if (part.type === PartType.kSegmentWildcard) {
// We only need to emit a regexp if a custom name was
// not specified. A custom name like `:foo` gets the
// kSegmentWildcard type automatically.
if (!customName) {
result += `(${segmentWildcardRegex})`;
}
} else if (part.type === PartType.kFullWildcard) {
// We can only use the `*` wildcard card if we meet a number
// of conditions. We must use an explicit `(.*)` group if:
//
// 1. A custom name was used; e.g. `:foo(.*)`.
// 2. If the preceding group is a matching group without a modifier; e.g.
// `(foo)(.*)`. In that case we cannot emit the `*` shorthand without
// it being mistakenly interpreted as the modifier for the previous
// group.
// 3. The current group is not enclosed in a `{ }` grouping.
// 4. The current group does not have an implicit prefix like `/`.
if (!customName && (!lastPart ||
lastPart.type === PartType.kFixed ||
lastPart.modifier !== Modifier.kNone ||
needsGrouping ||
part.prefix !== "")) {
result += "*";
} else {
result += `(${kFullWildcardRegex})`;
}
}
// If the matching group is a simple `:foo` custom name with the default
// segment wildcard, then we must check for a trailing suffix that could
// be interpreted as a trailing part of the name itself. In these cases
// we must escape the beginning of the suffix in order to separate it
// from the end of the custom name; e.g. `:foo\\bar` instead of `:foobar`.
if (part.type === PartType.kSegmentWildcard && customName &&
!!part.suffix.length) {
if (regexIdentifierPart.test(part.suffix[0])) {
result += '\\';
}
}
result += escapePatternString(part.suffix);
if (needsGrouping) {
result += '}';
}
if (part.modifier !== Modifier.kNone) {
result += modifierToString(part.modifier);
}
}
return result;
}
export class URLPattern {
#pattern: URLPatternInit;
#regexp: any = {};
#names: string[] = {};
#component_pattern: any = {};
#parts: any = {};
#hasRegExpGroups: boolean = false;
constructor(init: URLPatternInit | string, baseURL?: string, options?: URLPatternOptions);
constructor(init: URLPatternInit | string, options?: URLPatternOptions);
constructor(init: URLPatternInit | string = {}, baseURLOrOptions?: string | URLPatternOptions, options?: URLPatternOptions) {
try {
let baseURL = undefined;
if (typeof baseURLOrOptions === 'string') {
baseURL = baseURLOrOptions;
} else {
options = baseURLOrOptions;
}
if (typeof init === 'string') {
const parser = new Parser(init);
parser.parse();
init = parser.result;
if (baseURL === undefined && typeof init.protocol !== 'string') {
throw new TypeError(`A base URL must be provided for a relative constructor string.`);
}
init.baseURL = baseURL;
} else {
if (!init || typeof init !== 'object') {
throw new TypeError(`parameter 1 is not of type 'string' and cannot convert to dictionary.`);
}
if (baseURL) {
throw new TypeError(`parameter 1 is not of type 'string'.`);
}
}
if (typeof options === "undefined") {
options = { ignoreCase: false };
}
const ignoreCaseOptions = { ignoreCase: options.ignoreCase === true };
const defaults: URLPatternInit = {
pathname: DEFAULT_PATTERN,
protocol: DEFAULT_PATTERN,
username: DEFAULT_PATTERN,
password: DEFAULT_PATTERN,
hostname: DEFAULT_PATTERN,
port: DEFAULT_PATTERN,
search: DEFAULT_PATTERN,
hash: DEFAULT_PATTERN,
};
this.#pattern = applyInit(defaults, init, true);
if (defaultPortForProtocol(this.#pattern.protocol) === this.#pattern.port) {
this.#pattern.port = '';
}
let component: URLPatternKeys;
// Iterate in component order so we are sure to compile the protocol
// before the pathname. We need to know the protocol in order to know
// which kind of canonicalization to apply.
for (component of COMPONENTS) {
if (!(component in this.#pattern))
continue;
const options: Options & ParseOptions = {};
const pattern = this.#pattern[component];
this.#names[component] = [];
switch (component) {
case 'protocol':
Object.assign(options, DEFAULT_OPTIONS);
options.encodePart = protocolEncodeCallback;
break;
case 'username':
Object.assign(options, DEFAULT_OPTIONS);
options.encodePart = usernameEncodeCallback;
break;
case 'password':
Object.assign(options, DEFAULT_OPTIONS);
options.encodePart = passwordEncodeCallback;
break;
case 'hostname':
Object.assign(options, HOSTNAME_OPTIONS);
if (treatAsIPv6Hostname(pattern)) {
options.encodePart = ipv6HostnameEncodeCallback;
} else {
options.encodePart = hostnameEncodeCallback;
}
break;
case 'port':
Object.assign(options, DEFAULT_OPTIONS);
options.encodePart = portEncodeCallback;
break;
case 'pathname':
if (isSpecialScheme(this.#regexp.protocol)) {
Object.assign(options, PATHNAME_OPTIONS, ignoreCaseOptions);
options.encodePart = standardURLPathnameEncodeCallback;
} else {
Object.assign(options, DEFAULT_OPTIONS, ignoreCaseOptions);
options.encodePart = pathURLPathnameEncodeCallback;
}
break;
case 'search':
Object.assign(options, DEFAULT_OPTIONS, ignoreCaseOptions);
options.encodePart = searchEncodeCallback;
break;
case 'hash':
Object.assign(options, DEFAULT_OPTIONS, ignoreCaseOptions);
options.encodePart = hashEncodeCallback;
break;
}
try {
this.#parts[component] = parse(pattern as string, options);
this.#regexp[component] = partsToRegexp(this.#parts[component], /* out */ this.#names[component], options);
this.#component_pattern[component] = partsToPattern(this.#parts[component], options);
this.#hasRegExpGroups = this.#hasRegExpGroups ||
this.#parts[component].some((p: Part) => p.type === PartType.kRegex);
} catch (err) {
// If a pattern is illegal the constructor will throw an exception
throw new TypeError(`invalid ${component} pattern '${this.#pattern[component]}'.`);
}
}
} catch (err: any) {
throw new TypeError(`Failed to construct 'URLPattern': ${err.message}`);
}
}
get [Symbol.toStringTag]() {
return "URLPattern"
}
test(input: string | URLPatternInit = {}, baseURL?: string) {
let values: URLPatternInit = {
pathname: '',
protocol: '',
username: '',
password: '',
hostname: '',
port: '',
search: '',
hash: '',
};
if (typeof (input) !== 'string' && baseURL) {
throw new TypeError(`parameter 1 is not of type 'string'.`);
}
if (typeof input === 'undefined') {
return false;
}
try {
if (typeof input === 'object') {
values = applyInit(values, input, false);
} else {
values = applyInit(values, extractValues(input, baseURL), false);
}
} catch (err: any) {
// Treat exceptions simply as a failure to match.
return false;
}
let component: URLPatternKeys;
for (component of COMPONENTS) {
if (!this.#regexp[component].exec(values[component])) {
return false;
}
}
return true;
}
exec(input: string | URLPatternInit = {}, baseURL?: string): URLPatternResult | null | undefined {
let values: URLPatternInit = {
pathname: '',
protocol: '',
username: '',
password: '',
hostname: '',
port: '',
search: '',
hash: '',
};
if (typeof (input) !== 'string' && baseURL) {
throw new TypeError(`parameter 1 is not of type 'string'.`);
}
if (typeof input === 'undefined') {
return;
}
try {
if (typeof input === 'object') {
values = applyInit(values, input, false);
} else {
values = applyInit(values, extractValues(input, baseURL), false);
}
} catch (err: any) {
// Treat exceptions simply as a failure to match.
return null;
}
let result: any = {};
if (baseURL) {
result.inputs = [input, baseURL];
} else {
result.inputs = [input];
}
let component: URLPatternKeys;
for (component of COMPONENTS) {
let match = this.#regexp[component].exec(values[component]);
if (!match) {
return null;
}
let groups = {} as Array;
for (let [i, name] of this.#names[component].entries()) {
if (typeof name === 'string' || typeof name === 'number') {
let value = match[i + 1];
groups[name] = value;
}
}
result[component] = {
input: values[component] ?? '',
groups,
};
}
return result;
}
static compareComponent(component: URLPatternComponent, left: URLPattern, right: URLPattern) : Number {
const comparePart = (left: Part, right: Part) : Number => {
// We prioritize PartType in the ordering so we can favor fixed text. The
// type ordering is:
//
// kFixed > kRegex > kSegmentWildcard > kFullWildcard.
//
// We considered kRegex greater than the wildcards because it is likely to be
// used for imposing some constraint and not just duplicating wildcard
// behavior.
//
// This comparison depends on the PartType enum having the
// correct corresponding numeric values.
//
// Next the Modifier is considered:
//
// kNone > kOneOrMore > kOptional > kZeroOrMore.
//
// The rationale here is that requring the match group to exist is more
// restrictive then making it optional and requiring an exact count is more
// restrictive than repeating.
//
// This comparison depends on the Modifier enum in liburlpattern having the
// correct corresponding numeric values.
//
// Finally we lexicographically compare the text components from left to
// right; `prefix`, `value`, and `suffix`. It's OK to depend on simple
// byte-wise string comparison here because the values have all been URL
// encoded. This guarantees the strings contain only ASCII.
for (let attr of ["type", "modifier", "prefix", "value", "suffix"]) {
if (left[attr] < right[attr])
return -1;
else if (left[attr] === right[attr])
continue;
else
return 1;
}
return 0;
}
const emptyFixedPart: Part = new Part(PartType.kFixed, "", "", "", "", Modifier.kNone);
const wildcardOnlyPart: Part = new Part(PartType.kFullWildcard, "", "", "", "", Modifier.kNone);
const comparePartList = (left: Part[], right: Part[]) : Number => {
// Begin by comparing each Part in the lists with each other. If any
// are not equal, then we are done.
let i = 0;
for (; i < Math.min(left.length, right.length); ++i) {
let result = comparePart(left[i], right[i]);
if (result) // 1 or -1.
return result;
}
// No differences were found, so declare them equal.
if (left.length === right.length) {
return 0;
}
// We reached the end of at least one of the lists without finding a
// difference. However, we must handle the case where one list is longer
// than the other. In this case we compare the next Part from the
// longer list to a synthetically created empty kFixed Part. This is
// necessary in order for "/foo/" to be considered more restrictive, and
// therefore greater, than "/foo/*".
return comparePart(left[i] ?? emptyFixedPart, right[i] ?? emptyFixedPart);
}
// If both the left and right components are empty wildcards, then they are
// effectively equal.
if (!left.#component_pattern[component] && !right.#component_pattern[component]) {
return 0;
}
// If one side has a real pattern and the other side is an empty component,
// then we have to compare to a part list with a single full wildcard.
if (left.#component_pattern[component] && !right.#component_pattern[component]) {
return comparePartList(left.#parts[component], [wildcardOnlyPart]);
}
if (!left.#component_pattern[component] && right.#component_pattern[component]) {
return comparePartList([wildcardOnlyPart], right.#parts[component]);
}
// Otherwise compare the part lists of the patterns on each side.
return comparePartList(left.#parts[component], right.#parts[component]);
}
public get protocol() {
return this.#component_pattern.protocol;
}
public get username() {
return this.#component_pattern.username;
}
public get password() {
return this.#component_pattern.password;
}
public get hostname() {
return this.#component_pattern.hostname;
}
public get port() {
return this.#component_pattern.port;
}
public get pathname() {
return this.#component_pattern.pathname;
}
public get search() {
return this.#component_pattern.search;
}
public get hash() {
return this.#component_pattern.hash;
}
public get hasRegExpGroups() {
return this.#hasRegExpGroups;
}
}
================================================
FILE: src/url-utils.ts
================================================
import {ParseOptions, Options} from './path-to-regex-modified';
// default to strict mode and case sensitivity. In addition, most
// components have no concept of a delimiter or prefix character.
export const DEFAULT_OPTIONS: Options & ParseOptions = {
delimiter: '',
prefixes: '',
sensitive: true,
strict: true,
};
// The options to use for hostname patterns. This uses a
// "." delimiter controlling how far a named group like ":bar" will match
// by default. Note, hostnames are case insensitive but we require case
// sensitivity here. This assumes that the hostname values have already
// been normalized to lower case as in URL().
export const HOSTNAME_OPTIONS: Options & ParseOptions = {
delimiter: '.',
prefixes: '',
sensitive: true,
strict: true,
};
// The options to use for pathname patterns. This uses a
// "/" delimiter controlling how far a named group like ":bar" will match
// by default. It also configures "/" to be treated as an automatic
// prefix before groups.
export const PATHNAME_OPTIONS: Options & ParseOptions = {
delimiter: '/',
prefixes: '/',
sensitive: true,
strict: true,
};
// Utility function to determine if a pathname is absolute or not. For
// URL values this mainly consists of a check for a leading slash. For
// patterns we do some additional checking for escaped or grouped slashes.
export function isAbsolutePathname(pathname: string, isPattern: boolean): boolean {
if (!pathname.length) {
return false;
}
if (pathname[0] === '/') {
return true;
}
if (!isPattern) {
return false;
}
if (pathname.length < 2) {
return false;
}
// Patterns treat escaped slashes and slashes within an explicit grouping as
// valid leading slashes. For example, "\/foo" or "{/foo}". Patterns do
// not consider slashes within a custom regexp group as valid for the leading
// pathname slash for now. To support that we would need to be able to
// detect things like ":name_123(/foo)" as a valid leading group in a pattern,
// but that is considered too complex for now.
if ((pathname[0] == '\\' || pathname[0] == '{') && pathname[1] == '/') {
return true;
}
return false;
}
function maybeStripPrefix(value: string, prefix: string): string {
if (value.startsWith(prefix)) {
return value.substring(prefix.length, value.length);
}
return value;
}
function maybeStripSuffix(value: string, suffix: string): string {
if (value.endsWith(suffix)) {
return value.substr(0, value.length - suffix.length);
}
return value;
}
export function treatAsIPv6Hostname(value: string | undefined): boolean {
if (!value || value.length < 2) {
return false;
}
if (value[0] === '[') {
return true;
}
if ((value[0] === '\\' || value[0] === '{') &&
value[1] === '[') {
return true;
}
return false;
}
export const SPECIAL_SCHEMES = [
'ftp',
'file',
'http',
'https',
'ws',
'wss',
];
export function isSpecialScheme(protocol_regexp: any) {
if (!protocol_regexp) {
return true;
}
for (const scheme of SPECIAL_SCHEMES) {
if (protocol_regexp.test(scheme)) {
return true;
}
}
return false;
}
export function canonicalizeHash(hash: string, isPattern: boolean) {
hash = maybeStripPrefix(hash, '#');
if (isPattern || hash === '') {
return hash;
}
const url = new URL("https://example.com");
url.hash = hash;
return url.hash ? url.hash.substring(1, url.hash.length) : '';
}
export function canonicalizeSearch(search: string, isPattern: boolean) {
search = maybeStripPrefix(search, '?');
if (isPattern || search === '') {
return search;
}
const url = new URL("https://example.com");
url.search = search;
return url.search ? url.search.substring(1, url.search.length) : '';
}
export function canonicalizeHostname(hostname: string, isPattern: boolean) {
if (isPattern || hostname === '') {
return hostname;
}
if (treatAsIPv6Hostname(hostname)) {
return ipv6HostnameEncodeCallback(hostname);
} else {
return hostnameEncodeCallback(hostname);
}
}
export function canonicalizePassword(password: string, isPattern: boolean) {
if (isPattern || password === '') {
return password;
}
const url = new URL("https://example.com");
url.password = password;
return url.password;
}
export function canonicalizeUsername(username: string, isPattern: boolean) {
if (isPattern || username === '') {
return username;
}
const url = new URL("https://example.com");
url.username = username;
return url.username;
}
export function canonicalizePathname(pathname: string, protocol: string | undefined,
isPattern: boolean) {
if (isPattern || pathname === '') {
return pathname;
}
if (protocol && !SPECIAL_SCHEMES.includes(protocol)) {
const url = new URL(`${protocol}:${pathname}`);
return url.pathname;
}
const leadingSlash = pathname[0] == "/";
pathname = new URL(!leadingSlash ? '/-' + pathname : pathname,
"https://example.com").pathname;
if (!leadingSlash) {
pathname = pathname.substring(2, pathname.length);
}
return pathname;
}
export function canonicalizePort(port: string, protocol: string | undefined, isPattern: boolean): string {
if (defaultPortForProtocol(protocol) === port) {
port = '';
}
if (isPattern || port === '') {
return port;
}
return portEncodeCallback(port);
}
export function canonicalizeProtocol(protocol: string, isPattern: boolean) {
protocol = maybeStripSuffix(protocol, ':');
if (isPattern || protocol === '') {
return protocol;
}
return protocolEncodeCallback(protocol);
}
export function defaultPortForProtocol(protocol: string | undefined): string {
switch (protocol) {
case "ws":
case "http":
return '80';
case "wws":
case "https":
return '443';
case "ftp":
return '21';
default:
return '';
}
}
export function protocolEncodeCallback(input: string): string {
if (input === '') {
return input;
}
if (/^[-+.A-Za-z0-9]*$/.test(input))
return input.toLowerCase();
throw new TypeError(`Invalid protocol '${input}'.`);
}
export function usernameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL('https://example.com');
url.username = input;
return url.username;
}
export function passwordEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL('https://example.com');
url.password = input;
return url.password;
}
export function hostnameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
if (/[\t\n\r #%/:<>?@[\]^\\|]/g.test(input)) {
throw(new TypeError(`Invalid hostname '${input}'`));
}
const url = new URL('https://example.com');
url.hostname = input;
return url.hostname;
}
export function ipv6HostnameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
if (/[^0-9a-fA-F[\]:]/g.test(input)) {
throw(new TypeError(`Invalid IPv6 hostname '${input}'`));
}
return input.toLowerCase();
}
export function portEncodeCallback(input: string): string {
if (input === '') {
return input;
}
// Since ports only consist of digits there should be no encoding needed.
// Therefore we directly use the UTF8 encoding version of CanonicalizePort().
if ((/^[0-9]*$/.test(input) && parseInt(input) <= 65535)) {
return input;
}
throw new TypeError(`Invalid port '${input}'.`);
}
export function standardURLPathnameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL('https://example.com');
url.pathname = input[0] !== '/' ? '/-' + input : input;
if (input[0] !== '/') {
return url.pathname.substring(2, url.pathname.length);
}
return url.pathname;
}
export function pathURLPathnameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL(`data:${input}`);
return url.pathname;
}
export function searchEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL('https://example.com');
url.search = input;
return url.search.substring(1, url.search.length);
}
export function hashEncodeCallback(input: string): string {
if (input === '') {
return input;
}
const url = new URL('https://example.com');
url.hash = input;
return url.hash.substring(1, url.hash.length);
}
================================================
FILE: test/can-load-as-cms.cjs
================================================
require("urlpattern-polyfill");
const test = require("ava");
const baseURL = "https://example.com";
test("urlPattern", (t) => {
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
test("exports urlPattern", (t) => {
const { URLPattern } = require("urlpattern-polyfill");
t.true(typeof URLPattern === "function");
});
================================================
FILE: test/can-load-as-esm.mjs
================================================
import "urlpattern-polyfill";
import test from "ava";
const baseURL = "https://example.com";
test("urlPattern", (t) => {
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
test("export of urlPattern there?", async (t) => {
/** overwrite global wil local version imported, so we know the export is in place */
const { URLPattern } = await import("urlpattern-polyfill");
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
================================================
FILE: test/can-load-as-ts.ts
================================================
require("urlpattern-polyfill");
const test = require("ava");
const baseURL = "https://example.com";
test("urlPattern", (t) => {
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
test("exports urlPattern", (t) => {
const { URLPattern } = require("urlpattern-polyfill");
t.true(typeof URLPattern === "function");
});
================================================
FILE: test/ponyfill.cjs
================================================
const test = require("ava");
const { URLPattern } = require("urlpattern-polyfill/urlpattern");
const baseURL = "https://example.com";
test("urlPattern", (t) => {
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
test("does not pollute global scope", (t) => {
t.true(typeof globalThis.URLPattern === "undefined");
});
================================================
FILE: test/ponyfill.mjs
================================================
import test from "ava";
import { URLPattern } from "urlpattern-polyfill/urlpattern";
const baseURL = "https://example.com";
test("urlPattern", (t) => {
let pattern = new URLPattern({ baseURL, pathname: "/product/*?" });
t.true(pattern.test(baseURL + "/product/a/b"));
});
test("does not pollute global scope", (t) => {
t.true(typeof globalThis.URLPattern === "undefined");
});
================================================
FILE: test/urlpattern-compare-test-data.json
================================================
[
{
"component": "pathname",
"left": { "pathname": "/foo/a" },
"right": { "pathname": "/foo/b" },
"expected": -1
},
{
"component": "pathname",
"left": { "pathname": "/foo/b" },
"right": { "pathname": "/foo/bar" },
"expected": -1
},
{
"component": "pathname",
"left": { "pathname": "/foo/bar" },
"right": { "pathname": "/foo/:bar" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/" },
"right": { "pathname": "/foo/:bar" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/:bar" },
"right": { "pathname": "/foo/*" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/{bar}" },
"right": { "pathname": "/foo/(bar)" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/{bar}" },
"right": { "pathname": "/foo/{bar}+" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/{bar}+" },
"right": { "pathname": "/foo/{bar}?" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/{bar}?" },
"right": { "pathname": "/foo/{bar}*" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/(123)" },
"right": { "pathname": "/foo/(12)" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/:b" },
"right": { "pathname": "/foo/:a" },
"expected": 0
},
{
"component": "pathname",
"left": { "pathname": "*/foo" },
"right": { "pathname": "*" },
"expected": 1
},
{
"component": "port",
"left": { "port": "9" },
"right": { "port": "100" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "foo/:bar?/baz" },
"right": { "pathname": "foo/{:bar}?/baz" },
"expected": -1
},
{
"component": "pathname",
"left": { "pathname": "foo/:bar?/baz" },
"right": { "pathname": "foo{/:bar}?/baz" },
"expected": 0
},
{
"component": "pathname",
"left": { "pathname": "foo/:bar?/baz" },
"right": { "pathname": "fo{o/:bar}?/baz" },
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "foo/:bar?/baz" },
"right": { "pathname": "foo{/:bar/}?baz" },
"expected": -1
},
{
"component": "pathname",
"left": "https://a.example.com/b?a",
"right": "https://b.example.com/a?b",
"expected": 1
},
{
"component": "pathname",
"left": { "pathname": "/foo/{bar}/baz" },
"right": { "pathname": "/foo/bar/baz" },
"expected": 0
},
{
"component": "protocol",
"left": { "protocol": "a" },
"right": { "protocol": "b" },
"expected": -1
},
{
"component": "username",
"left": { "username": "a" },
"right": { "username": "b" },
"expected": -1
},
{
"component": "password",
"left": { "password": "a" },
"right": { "password": "b" },
"expected": -1
},
{
"component": "hostname",
"left": { "hostname": "a" },
"right": { "hostname": "b" },
"expected": -1
},
{
"component": "search",
"left": { "search": "a" },
"right": { "search": "b" },
"expected": -1
},
{
"component": "hash",
"left": { "hash": "a" },
"right": { "hash": "b" },
"expected": -1
}
]
================================================
FILE: test/urlpattern-compare-tests.js
================================================
import test from "ava";
import { readFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import "urlpattern-polyfill";
function runTests(data) {
for (let entry of data) {
test(`Component: ${entry.component} ` +
`Left: ${JSON.stringify(entry.left)} ` +
`Right: ${JSON.stringify(entry.right)}`, t => {
const assert_equals = (actual, expected, msg) => t.is(actual, expected, msg);
const left = new URLPattern(entry.left);
const right = new URLPattern(entry.right);
assert_equals(URLPattern.compareComponent(entry.component, left, right), entry.expected);
// We have to coerce to an integer here in order to avoid asserting
// that `+0` is `-0`.
const reverse_expected = ~~(entry.expected * -1);
assert_equals(URLPattern.compareComponent(entry.component, right, left), reverse_expected, "reverse order");
assert_equals(URLPattern.compareComponent(entry.component, left, left), 0, "left equality");
assert_equals(URLPattern.compareComponent(entry.component, right, right), 0, "right equality");
});
}
}
let path = fileURLToPath(import.meta.url);
let data = readFileSync(resolve(dirname(path), "urlpattern-compare-test-data.json"));
runTests(JSON.parse(data));
================================================
FILE: test/urlpattern-hasregexpgroups-tests.js
================================================
import test from "ava";
import "urlpattern-polyfill";
test("URLPattern.hasRegExpGroups", t => {
const assert_true = (actual, msg) => t.true(actual, msg);
const assert_false = (actual, msg) => t.false(actual, msg);
assert_true('hasRegExpGroups' in URLPattern.prototype, "hasRegExpGroups is not implemented");
assert_false(new URLPattern({}).hasRegExpGroups, "match-everything pattern");
for (let component of ['protocol', 'username', 'password', 'hostname', 'port', 'pathname', 'search', 'hash']) {
assert_false(new URLPattern({[component]: '*'}).hasRegExpGroups, `wildcard in ${component}`);
assert_false(new URLPattern({[component]: ':foo'}).hasRegExpGroups, `segment wildcard in ${component}`);
assert_false(new URLPattern({[component]: ':foo?'}).hasRegExpGroups, `optional segment wildcard in ${component}`);
assert_true(new URLPattern({[component]: ':foo(hi)'}).hasRegExpGroups, `named regexp group in ${component}`);
assert_true(new URLPattern({[component]: '(hi)'}).hasRegExpGroups, `anonymous regexp group in ${component}`);
if (component !== 'protocol' && component !== 'port') {
// These components are more narrow in what they accept in any case.
assert_false(new URLPattern({[component]: 'a-{:hello}-z-*-a'}).hasRegExpGroups, `wildcards mixed in with fixed text and wildcards in ${component}`);
assert_true(new URLPattern({[component]: 'a-(hi)-z-(lo)-a'}).hasRegExpGroups, `regexp groups mixed in with fixed text and wildcards in ${component}`);
}
}
assert_false(new URLPattern({pathname: '/a/:foo/:baz?/b/*'}).hasRegExpGroups, "complex pathname with no regexp");
assert_true(new URLPattern({pathname: '/a/:foo/:baz([a-z]+)?/b/*'}).hasRegExpGroups, "complex pathname with regexp");
});
================================================
FILE: test/urlpatterntestdata.json
================================================
[
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/ba" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/bar/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [ "https://example.com/foo/bar" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": { "0": "example.com" } },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": { "0": "https" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [ "https://example.com/foo/bar/baz" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }],
"expected_match": {
"hostname": { "input": "example.com", "groups": { "0": "example.com" } },
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "hostname": "example.com", "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }],
"expected_match": {
"hostname": { "input": "example.com", "groups": { "0": "example.com" } },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": { "0": "https" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/bar/baz",
"baseURL": "https://example.com" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar", "search": "otherquery",
"hash": "otherhash" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hash": { "input": "otherhash", "groups": { "0": "otherhash" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "otherquery", "groups": { "0": "otherquery" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar", "search": "otherquery",
"hash": "otherhash" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hash": { "input": "otherhash", "groups": { "0": "otherhash" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "otherquery", "groups": { "0": "otherquery" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?otherquery#otherhash" }],
"inputs": [{ "protocol": "https", "hostname": "example.com",
"pathname": "/foo/bar", "search": "otherquery",
"hash": "otherhash" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hash": { "input": "otherhash", "groups": { "0": "otherhash" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "otherquery", "groups": { "0": "otherquery" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "https://example.com/foo/bar" ],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "https://example.com/foo/bar?otherquery#otherhash" ],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hash": { "input": "otherhash", "groups": { "0": "otherhash" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "otherquery", "groups": { "0": "otherquery" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "https://example.com/foo/bar?query#hash" ],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hash": { "input": "hash", "groups": { "0": "hash" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "query", "groups": { "0": "query" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "https://example.com/foo/bar/baz" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "https://other.com/foo/bar" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [ "http://other.com/foo/bar" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar/baz",
"baseURL": "https://example.com" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar", "baseURL": "https://other.com" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar",
"baseURL": "https://example.com?query#hash" }],
"inputs": [{ "pathname": "/foo/bar", "baseURL": "http://example.com" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/([^\\/]+?)" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar" }],
"inputs": [{ "pathname": "/foo/index.html" }],
"expected_match": {
"pathname": { "input": "/foo/index.html", "groups": { "bar": "index.html" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar" }],
"inputs": [{ "pathname": "/foo/bar/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_obj": {
"pathname": "/foo/*"
},
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/*" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_obj": {
"pathname": "/foo/*"
},
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/*" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_obj": {
"pathname": "/foo/*"
},
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/*" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)" }],
"inputs": [{ "pathname": "/foo" }],
"expected_obj": {
"pathname": "/foo/*"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar(.*)" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar(.*)" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar(.*)" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "bar": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar(.*)" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar?" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar?" }],
"inputs": [{ "pathname": "/foo" }],
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "bar": null } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar?" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar?" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar?" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar+" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar+" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar+" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar+" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar+" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar*" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar*" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar*" }],
"inputs": [{ "pathname": "/foo" }],
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "bar": null } }
}
},
{
"pattern": [{ "pathname": "/foo/:bar*" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/:bar*" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/foo" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "0": null } }
}
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/foo" }],
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "0": null } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)?" }],
"inputs": [{ "pathname": "/fo" }],
"expected_obj": {
"pathname": "/foo/*?"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*?" }],
"inputs": [{ "pathname": "/fo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/foo" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)+" }],
"inputs": [{ "pathname": "/fo" }],
"expected_obj": {
"pathname": "/foo/*+"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/*+" }],
"inputs": [{ "pathname": "/fo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_obj": {
"pathname": "/foo/**"
},
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": { "0": "bar" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_obj": {
"pathname": "/foo/**"
},
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": {
"pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/foo" }],
"expected_obj": {
"pathname": "/foo/**"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "0": null } }
}
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/foo" }],
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "/foo", "groups": { "0": null } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_obj": {
"pathname": "/foo/**"
},
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": {
"pathname": { "input": "/foo/", "groups": { "0": "" } }
}
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_obj": {
"pathname": "/foo/**"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/foobar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/(.*)*" }],
"inputs": [{ "pathname": "/fo" }],
"expected_obj": {
"pathname": "/foo/**"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/**" }],
"inputs": [{ "pathname": "/fo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}" }],
"inputs": [{ "pathname": "/foo" }],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}?" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}?" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}?" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": {
"pathname": { "input": "/foo", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}?" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}+" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}+" }],
"inputs": [{ "pathname": "/foo/bar/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}+" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}+" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}+" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}*" }],
"inputs": [{ "pathname": "/foo/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}*" }],
"inputs": [{ "pathname": "/foo/bar/bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar/bar", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}*" }],
"inputs": [{ "pathname": "/foo/bar/baz" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo{/bar}*" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": {
"pathname": { "input": "/foo", "groups": {} }
}
},
{
"pattern": [{ "pathname": "/foo{/bar}*" }],
"inputs": [{ "pathname": "/foo/" }],
"expected_match": null
},
{
"pattern": [{ "protocol": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "username": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "password": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "pathname": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "search": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "hash": "(café)" }],
"expected_obj": "error"
},
{
"pattern": [{ "protocol": ":café" }],
"inputs": [{ "protocol": "foo" }],
"expected_match": {
"protocol": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "username": ":café" }],
"inputs": [{ "username": "foo" }],
"expected_match": {
"username": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "password": ":café" }],
"inputs": [{ "password": "foo" }],
"expected_match": {
"password": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "hostname": ":café" }],
"inputs": [{ "hostname": "foo" }],
"expected_match": {
"hostname": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "pathname": "/:café" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": {
"pathname": { "input": "/foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "search": ":café" }],
"inputs": [{ "search": "foo" }],
"expected_match": {
"search": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "hash": ":café" }],
"inputs": [{ "hash": "foo" }],
"expected_match": {
"hash": { "input": "foo", "groups": { "café": "foo" } }
}
},
{
"pattern": [{ "protocol": ":\u2118" }],
"inputs": [{ "protocol": "foo" }],
"expected_match": {
"protocol": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "username": ":\u2118" }],
"inputs": [{ "username": "foo" }],
"expected_match": {
"username": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "password": ":\u2118" }],
"inputs": [{ "password": "foo" }],
"expected_match": {
"password": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "hostname": ":\u2118" }],
"inputs": [{ "hostname": "foo" }],
"expected_match": {
"hostname": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "pathname": "/:\u2118" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": {
"pathname": { "input": "/foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "search": ":\u2118" }],
"inputs": [{ "search": "foo" }],
"expected_match": {
"search": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "hash": ":\u2118" }],
"inputs": [{ "hash": "foo" }],
"expected_match": {
"hash": { "input": "foo", "groups": { "\u2118": "foo" } }
}
},
{
"pattern": [{ "protocol": ":\u3400" }],
"inputs": [{ "protocol": "foo" }],
"expected_match": {
"protocol": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "username": ":\u3400" }],
"inputs": [{ "username": "foo" }],
"expected_match": {
"username": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "password": ":\u3400" }],
"inputs": [{ "password": "foo" }],
"expected_match": {
"password": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "hostname": ":\u3400" }],
"inputs": [{ "hostname": "foo" }],
"expected_match": {
"hostname": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "pathname": "/:\u3400" }],
"inputs": [{ "pathname": "/foo" }],
"expected_match": {
"pathname": { "input": "/foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "search": ":\u3400" }],
"inputs": [{ "search": "foo" }],
"expected_match": {
"search": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "hash": ":\u3400" }],
"inputs": [{ "hash": "foo" }],
"expected_match": {
"hash": { "input": "foo", "groups": { "\u3400": "foo" } }
}
},
{
"pattern": [{ "protocol": "(.*)" }],
"inputs": [{ "protocol" : "café" }],
"expected_obj": {
"protocol": "*"
},
"expected_match": null
},
{
"pattern": [{ "protocol": "(.*)" }],
"inputs": [{ "protocol": "cafe" }],
"expected_obj": {
"protocol": "*"
},
"expected_match": {
"protocol": { "input": "cafe", "groups": { "0": "cafe" }}
}
},
{
"pattern": [{ "protocol": "foo-bar" }],
"inputs": [{ "protocol": "foo-bar" }],
"expected_match": {
"protocol": { "input": "foo-bar", "groups": {} }
}
},
{
"pattern": [{ "username": "caf%C3%A9" }],
"inputs": [{ "username" : "café" }],
"expected_match": {
"username": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "username": "café" }],
"inputs": [{ "username" : "café" }],
"expected_obj": {
"username": "caf%C3%A9"
},
"expected_match": {
"username": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "username": "caf%c3%a9" }],
"inputs": [{ "username" : "café" }],
"expected_match": null
},
{
"pattern": [{ "password": "caf%C3%A9" }],
"inputs": [{ "password" : "café" }],
"expected_match": {
"password": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "password": "café" }],
"inputs": [{ "password" : "café" }],
"expected_obj": {
"password": "caf%C3%A9"
},
"expected_match": {
"password": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "password": "caf%c3%a9" }],
"inputs": [{ "password" : "café" }],
"expected_match": null
},
{
"pattern": [{ "hostname": "xn--caf-dma.com" }],
"inputs": [{ "hostname" : "café.com" }],
"expected_match": {
"hostname": { "input": "xn--caf-dma.com", "groups": {}}
}
},
{
"pattern": [{ "hostname": "café.com" }],
"inputs": [{ "hostname" : "café.com" }],
"expected_obj": {
"hostname": "xn--caf-dma.com"
},
"expected_match": {
"hostname": { "input": "xn--caf-dma.com", "groups": {}}
}
},
{
"pattern": [{ "port": "" }],
"inputs": [{ "protocol": "http", "port": "80" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"protocol": { "input": "http", "groups": { "0": "http" }}
}
},
{
"pattern": [{ "protocol": "http", "port": "80" }],
"inputs": [{ "protocol": "http", "port": "80" }],
"exactly_empty_components": [ "port" ],
"expected_match": {
"protocol": { "input": "http", "groups": {}}
}
},
{
"pattern": [{ "protocol": "http", "port": "80{20}?" }],
"inputs": [{ "protocol": "http", "port": "80" }],
"expected_match": null
},
{
"pattern": [{ "protocol": "http", "port": "80 " }],
"inputs": [{ "protocol": "http", "port": "80" }],
"expected_obj": "error"
},
{
"pattern": [{ "port": "80" }],
"inputs": [{ "protocol": "http", "port": "80" }],
"expected_match": null
},
{
"pattern": [{ "protocol": "http{s}?", "port": "80" }],
"inputs": [{ "protocol": "http", "port": "80" }],
"expected_match": null
},
{
"pattern": [{ "port": "80" }],
"inputs": [{ "port": "80" }],
"expected_match": {
"port": { "input": "80", "groups": {}}
}
},
{
"pattern": [{ "port": "(.*)" }],
"inputs": [{ "port": "invalid80" }],
"expected_obj": {
"port": "*"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "/foo/./bar" }],
"expected_match": {
"pathname": { "input": "/foo/bar", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo/baz" }],
"inputs": [{ "pathname": "/foo/bar/../baz" }],
"expected_match": {
"pathname": { "input": "/foo/baz", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/caf%C3%A9" }],
"inputs": [{ "pathname": "/café" }],
"expected_match": {
"pathname": { "input": "/caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/café" }],
"inputs": [{ "pathname": "/café" }],
"expected_obj": {
"pathname": "/caf%C3%A9"
},
"expected_match": {
"pathname": { "input": "/caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/caf%c3%a9" }],
"inputs": [{ "pathname": "/café" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "foo/bar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }],
"expected_match": {
"protocol": { "input": "https", "groups": { "0": "https" }},
"hostname": { "input": "example.com", "groups": { "0": "example.com" }},
"pathname": { "input": "/foo/bar", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo/../bar" }],
"inputs": [{ "pathname": "/bar" }],
"expected_obj": {
"pathname": "/bar"
},
"expected_match": {
"pathname": { "input": "/bar", "groups": {}}
}
},
{
"pattern": [{ "pathname": "./foo/bar", "baseURL": "https://example.com" }],
"inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": {
"protocol": { "input": "https", "groups": {}},
"hostname": { "input": "example.com", "groups": {}},
"pathname": { "input": "/foo/bar", "groups": {}}
}
},
{
"pattern": [{ "pathname": "", "baseURL": "https://example.com" }],
"inputs": [{ "pathname": "/", "baseURL": "https://example.com" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "https", "groups": {}},
"hostname": { "input": "example.com", "groups": {}},
"pathname": { "input": "/", "groups": {}}
}
},
{
"pattern": [{ "pathname": "{/bar}", "baseURL": "https://example.com/foo/" }],
"inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/bar"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "\\/bar", "baseURL": "https://example.com/foo/" }],
"inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/bar"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "b", "baseURL": "https://example.com/foo/" }],
"inputs": [{ "pathname": "./b", "baseURL": "https://example.com/foo/" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/foo/b"
},
"expected_match": {
"protocol": { "input": "https", "groups": {}},
"hostname": { "input": "example.com", "groups": {}},
"pathname": { "input": "/foo/b", "groups": {}}
}
},
{
"pattern": [{ "pathname": "foo/bar" }],
"inputs": [ "https://example.com/foo/bar" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }],
"inputs": [ "https://example.com/foo/bar" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/foo/bar"
},
"expected_match": {
"protocol": { "input": "https", "groups": {}},
"hostname": { "input": "example.com", "groups": {}},
"pathname": { "input": "/foo/bar", "groups": {}}
}
},
{
"pattern": [{ "pathname": ":name.html", "baseURL": "https://example.com" }],
"inputs": [ "https://example.com/foo.html"] ,
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/:name.html"
},
"expected_match": {
"protocol": { "input": "https", "groups": {}},
"hostname": { "input": "example.com", "groups": {}},
"pathname": { "input": "/foo.html", "groups": { "name": "foo" }}
}
},
{
"pattern": [{ "search": "q=caf%C3%A9" }],
"inputs": [{ "search": "q=café" }],
"expected_match": {
"search": { "input": "q=caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "search": "q=café" }],
"inputs": [{ "search": "q=café" }],
"expected_obj": {
"search": "q=caf%C3%A9"
},
"expected_match": {
"search": { "input": "q=caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "search": "q=caf%c3%a9" }],
"inputs": [{ "search": "q=café" }],
"expected_match": null
},
{
"pattern": [{ "hash": "caf%C3%A9" }],
"inputs": [{ "hash": "café" }],
"expected_match": {
"hash": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "hash": "café" }],
"inputs": [{ "hash": "café" }],
"expected_obj": {
"hash": "caf%C3%A9"
},
"expected_match": {
"hash": { "input": "caf%C3%A9", "groups": {}}
}
},
{
"pattern": [{ "hash": "caf%c3%a9" }],
"inputs": [{ "hash": "café" }],
"expected_match": null
},
{
"pattern": [{ "protocol": "about", "pathname": "(blank|sourcedoc)" }],
"inputs": [ "about:blank" ],
"expected_match": {
"protocol": { "input": "about", "groups": {}},
"pathname": { "input": "blank", "groups": { "0": "blank" }}
}
},
{
"pattern": [{ "protocol": "data", "pathname": ":number([0-9]+)" }],
"inputs": [ "data:8675309" ],
"expected_match": {
"protocol": { "input": "data", "groups": {}},
"pathname": { "input": "8675309", "groups": { "number": "8675309" }}
}
},
{
"pattern": [{ "pathname": "/(\\m)" }],
"expected_obj": "error"
},
{
"pattern": [{ "pathname": "/foo!" }],
"inputs": [{ "pathname": "/foo!" }],
"expected_match": {
"pathname": { "input": "/foo!", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo\\:" }],
"inputs": [{ "pathname": "/foo:" }],
"expected_match": {
"pathname": { "input": "/foo:", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo\\{" }],
"inputs": [{ "pathname": "/foo{" }],
"expected_obj": {
"pathname": "/foo%7B"
},
"expected_match": {
"pathname": { "input": "/foo%7B", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo\\(" }],
"inputs": [{ "pathname": "/foo(" }],
"expected_match": {
"pathname": { "input": "/foo(", "groups": {}}
}
},
{
"pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"expected_match": {
"protocol": { "input": "javascript", "groups": {}},
"pathname": { "input": "var x = 1;", "groups": {}}
}
},
{
"pattern": [{ "pathname": "var x = 1;" }],
"inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"expected_obj": {
"pathname": "var%20x%20=%201;"
},
"expected_match": null
},
{
"pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"inputs": [{ "baseURL": "javascript:var x = 1;" }],
"expected_match": {
"protocol": { "input": "javascript", "groups": {}},
"pathname": { "input": "var x = 1;", "groups": {}}
}
},
{
"pattern": [{ "protocol": "(data|javascript)", "pathname": "var x = 1;" }],
"inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"expected_match": {
"protocol": { "input": "javascript", "groups": {"0": "javascript"}},
"pathname": { "input": "var x = 1;", "groups": {}}
}
},
{
"pattern": [{ "protocol": "(https|javascript)", "pathname": "var x = 1;" }],
"inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }],
"expected_obj": {
"pathname": "var%20x%20=%201;"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "var x = 1;" }],
"inputs": [{ "pathname": "var x = 1;" }],
"expected_obj": {
"pathname": "var%20x%20=%201;"
},
"expected_match": {
"pathname": { "input": "var%20x%20=%201;", "groups": {}}
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [ "./foo/bar", "https://example.com" ],
"expected_match": {
"hostname": { "input": "example.com", "groups": { "0": "example.com" } },
"pathname": { "input": "/foo/bar", "groups": {} },
"protocol": { "input": "https", "groups": { "0": "https" } }
}
},
{
"pattern": [{ "pathname": "/foo/bar" }],
"inputs": [ { "pathname": "/foo/bar" }, "https://example.com" ],
"expected_match": "error"
},
{
"pattern": [ "https://example.com:8080/foo?bar#baz" ],
"inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz",
"baseURL": "https://example.com:8080" }],
"expected_obj": {
"protocol": "https",
"username": "*",
"password": "*",
"hostname": "example.com",
"port": "8080",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"search": { "input": "bar", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [ "/foo?bar#baz", "https://example.com:8080" ],
"inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz",
"baseURL": "https://example.com:8080" }],
"expected_obj": {
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"search": { "input": "bar", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [ "/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "example.com/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "http{s}?://{*.}?example.com/:product/:endpoint" ],
"inputs": [ "https://sub.example.com/foo/bar" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "http{s}?",
"hostname": "{*.}?example.com",
"pathname": "/:product/:endpoint"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "sub.example.com", "groups": { "0": "sub" } },
"pathname": { "input": "/foo/bar", "groups": { "product": "foo",
"endpoint": "bar" } }
}
},
{
"pattern": [ "https://example.com?foo" ],
"inputs": [ "https://example.com/?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com#foo" ],
"inputs": [ "https://example.com/#foo" ],
"exactly_empty_components": [ "port", "search" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/",
"hash": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"hash": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com:8080?foo" ],
"inputs": [ "https://example.com:8080/?foo" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"port": "8080",
"pathname": "/",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com:8080#foo" ],
"inputs": [ "https://example.com:8080/#foo" ],
"exactly_empty_components": [ "search" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"port": "8080",
"pathname": "/",
"hash": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"hash": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/?foo" ],
"inputs": [ "https://example.com/?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/#foo" ],
"inputs": [ "https://example.com/#foo" ],
"exactly_empty_components": [ "port", "search" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/",
"hash": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"hash": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/*?foo" ],
"inputs": [ "https://example.com/?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/*?foo"
},
"expected_match": null
},
{
"pattern": [ "https://example.com/*\\?foo" ],
"inputs": [ "https://example.com/?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/*",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "" } },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/:name?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/:name?foo"
},
"expected_match": null
},
{
"pattern": [ "https://example.com/:name\\?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/:name",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/bar", "groups": { "name": "bar" } },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/(bar)?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/(bar)?foo"
},
"expected_match": null
},
{
"pattern": [ "https://example.com/(bar)\\?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/(bar)",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/bar", "groups": { "0": "bar" } },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/{bar}?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/{bar}?foo"
},
"expected_match": null
},
{
"pattern": [ "https://example.com/{bar}\\?foo" ],
"inputs": [ "https://example.com/bar?foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/bar",
"search": "foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/bar", "groups": {} },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "https://example.com/" ],
"inputs": [ "https://example.com:8080/" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"port": "",
"pathname": "/"
},
"expected_match": null
},
{
"pattern": [ "data:foobar" ],
"inputs": [ "data:foobar" ],
"expected_obj": "error"
},
{
"pattern": [ "data\\:foobar" ],
"inputs": [ "data:foobar" ],
"exactly_empty_components": [ "hostname", "port" ],
"expected_obj": {
"protocol": "data",
"pathname": "foobar"
},
"expected_match": {
"protocol": { "input": "data", "groups": {} },
"pathname": { "input": "foobar", "groups": {} }
}
},
{
"pattern": [ "https://{sub.}?example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "{sub.}?example.com",
"pathname": "/foo"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo", "groups": {} }
}
},
{
"pattern": [ "https://{sub.}?example{.com/}foo" ],
"inputs": [ "https://example.com/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "{https://}example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "https://(sub.)?example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "(sub.)?example.com",
"pathname": "/foo"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": { "0": null } },
"pathname": { "input": "/foo", "groups": {} }
}
},
{
"pattern": [ "https://(sub.)?example(.com/)foo" ],
"inputs": [ "https://example.com/foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "(sub.)?example(.com/)foo",
"pathname": "*"
},
"expected_match": null
},
{
"pattern": [ "(https://)example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "https://{sub{.}}example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"expected_obj": "error"
},
{
"pattern": [ "https://(sub(?:.))?example.com/foo" ],
"inputs": [ "https://example.com/foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "(sub(?:.))?example.com",
"pathname": "/foo"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": { "0": null } },
"pathname": { "input": "/foo", "groups": {} }
}
},
{
"pattern": [ "file:///foo/bar" ],
"inputs": [ "file:///foo/bar" ],
"exactly_empty_components": [ "hostname", "port" ],
"expected_obj": {
"protocol": "file",
"pathname": "/foo/bar"
},
"expected_match": {
"protocol": { "input": "file", "groups": {} },
"pathname": { "input": "/foo/bar", "groups": {} }
}
},
{
"pattern": [ "data:" ],
"inputs": [ "data:" ],
"exactly_empty_components": [ "hostname", "port", "pathname" ],
"expected_obj": {
"protocol": "data"
},
"expected_match": {
"protocol": { "input": "data", "groups": {} }
}
},
{
"pattern": [ "foo://bar" ],
"inputs": [ "foo://bad_url_browser_interop" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "foo",
"hostname": "bar"
},
"expected_match": null
},
{
"pattern": [ "(café)://foo" ],
"expected_obj": "error"
},
{
"pattern": [ "https://example.com/foo?bar#baz" ],
"inputs": [{ "protocol": "https:",
"search": "?bar",
"hash": "#baz",
"baseURL": "http://example.com/foo" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": null
},
{
"pattern": [{ "protocol": "http{s}?:",
"search": "?bar",
"hash": "#baz" }],
"inputs": [ "http://example.com/foo?bar#baz" ],
"expected_obj": {
"protocol": "http{s}?",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "example.com", "groups": { "0": "example.com" }},
"pathname": { "input": "/foo", "groups": { "0": "/foo" }},
"search": { "input": "bar", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [ "?bar#baz", "https://example.com/foo" ],
"inputs": [ "?bar#baz", "https://example.com/foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"search": { "input": "bar", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [ "?bar", "https://example.com/foo#baz" ],
"inputs": [ "?bar", "https://example.com/foo#snafu" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/foo",
"search": "bar",
"hash": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"search": { "input": "bar", "groups": {} }
}
},
{
"pattern": [ "#baz", "https://example.com/foo?bar" ],
"inputs": [ "#baz", "https://example.com/foo?bar" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"search": { "input": "bar", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [ "#baz", "https://example.com/foo" ],
"inputs": [ "#baz", "https://example.com/foo" ],
"exactly_empty_components": [ "port", "search" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/foo",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/foo", "groups": {} },
"hash": { "input": "baz", "groups": {} }
}
},
{
"pattern": [{ "pathname": "*" }],
"inputs": [ "foo", "data:data-urls-cannot-be-base-urls" ],
"expected_match": null
},
{
"pattern": [{ "pathname": "*" }],
"inputs": [ "foo", "not|a|valid|url" ],
"expected_match": null
},
{
"pattern": [ "https://foo\\:bar@example.com" ],
"inputs": [ "https://foo:bar@example.com" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"username": "foo",
"password": "bar",
"hostname": "example.com",
"pathname": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"username": { "input": "foo", "groups": {} },
"password": { "input": "bar", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "/" } }
}
},
{
"pattern": [ "https://foo@example.com" ],
"inputs": [ "https://foo@example.com" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"username": "foo",
"hostname": "example.com",
"pathname": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"username": { "input": "foo", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "/" } }
}
},
{
"pattern": [ "https://\\:bar@example.com" ],
"inputs": [ "https://:bar@example.com" ],
"exactly_empty_components": [ "username", "port" ],
"expected_obj": {
"protocol": "https",
"password": "bar",
"hostname": "example.com",
"pathname": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"password": { "input": "bar", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "/" } }
}
},
{
"pattern": [ "https://:user::pass@example.com" ],
"inputs": [ "https://foo:bar@example.com" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"username": ":user",
"password": ":pass",
"hostname": "example.com",
"pathname": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"username": { "input": "foo", "groups": { "user": "foo" } },
"password": { "input": "bar", "groups": { "pass": "bar" } },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "/" } }
}
},
{
"pattern": [ "https\\:foo\\:bar@example.com" ],
"inputs": [ "https:foo:bar@example.com" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"username": "foo",
"password": "bar",
"hostname": "example.com",
"pathname": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"username": { "input": "foo", "groups": {} },
"password": { "input": "bar", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": { "0": "/" } }
}
},
{
"pattern": [ "data\\:foo\\:bar@example.com" ],
"inputs": [ "data:foo:bar@example.com" ],
"exactly_empty_components": [ "hostname", "port" ],
"expected_obj": {
"protocol": "data",
"pathname": "foo\\:bar@example.com"
},
"expected_match": {
"protocol": { "input": "data", "groups": {} },
"pathname": { "input": "foo:bar@example.com", "groups": {} }
}
},
{
"pattern": [ "https://foo{\\:}bar@example.com" ],
"inputs": [ "https://foo:bar@example.com" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"username": "foo%3Abar",
"hostname": "example.com"
},
"expected_match": null
},
{
"pattern": [ "data{\\:}channel.html", "https://example.com" ],
"inputs": [ "https://example.com/data:channel.html" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"pathname": "/data\\:channel.html",
"search": "*",
"hash": "*"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/data:channel.html", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:1]/" ],
"inputs": [ "http://[::1]/" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:1]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:1]:8080/" ],
"inputs": [ "http://[::1]:8080/" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:1]",
"port": "8080",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:a]/" ],
"inputs": [ "http://[::a]/" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:a]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::a]", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[:address]/" ],
"inputs": [ "http://[::1]/" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "http",
"hostname": "[:address]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": { "address": "::1" }},
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:AB\\::num]/" ],
"inputs": [ "http://[::ab:1]/" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:ab\\::num]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }},
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [{ "hostname": "[\\:\\:AB\\::num]" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_obj": {
"hostname": "[\\:\\:ab\\::num]"
},
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}
}
},
{
"pattern": [{ "hostname": "[\\:\\:xY\\::num]" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "{[\\:\\:ab\\::num]}" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}
}
},
{
"pattern": [{ "hostname": "{[\\:\\:fé\\::num]}" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "{[\\:\\::num\\:1]}" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "ab" }}
}
},
{
"pattern": [{ "hostname": "{[\\:\\::num\\:fé]}" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "[*\\:1]" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "0": "::ab" }}
}
},
{
"pattern": [{ "hostname": "*\\:1]" }],
"expected_obj": "error"
},
{
"pattern": [ "https://foo{{@}}example.com" ],
"inputs": [ "https://foo@example.com" ],
"expected_obj": "error"
},
{
"pattern": [ "https://foo{@example.com" ],
"inputs": [ "https://foo@example.com" ],
"expected_obj": "error"
},
{
"pattern": [ "data\\:text/javascript,let x = 100/:tens?5;" ],
"inputs": [ "data:text/javascript,let x = 100/5;" ],
"exactly_empty_components": [ "hostname", "port" ],
"expected_obj": {
"protocol": "data",
"pathname": "text/javascript,let x = 100/:tens?5;"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"protocol": { "input": "data", "groups": {} },
"pathname": { "input": "text/javascript,let x = 100/5;", "groups": { "tens": null } }
}
},
{
"pattern": [{ "pathname": "/:id/:id" }],
"expected_obj": "error"
},
{
"pattern": [{ "pathname": "/foo", "baseURL": "" }],
"expected_obj": "error"
},
{
"pattern": [ "/foo", "" ],
"expected_obj": "error"
},
{
"pattern": [{ "pathname": "/foo" }, "https://example.com" ],
"expected_obj": "error"
},
{
"pattern": [{ "pathname": ":name*" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "pathname": ":name+" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "pathname": ":name" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "protocol": ":name*" }],
"inputs": [{ "protocol": "foobar" }],
"expected_match": {
"protocol": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "protocol": ":name+" }],
"inputs": [{ "protocol": "foobar" }],
"expected_match": {
"protocol": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "protocol": ":name" }],
"inputs": [{ "protocol": "foobar" }],
"expected_match": {
"protocol": { "input": "foobar", "groups": { "name": "foobar" }}
}
},
{
"pattern": [{ "hostname": "bad hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad#hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad%hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad/hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad\\:hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "badhostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad?hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad@hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad[hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad]hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad\\\\hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad^hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad|hostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad\nhostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad\rhostname" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "bad\thostname" }],
"expected_obj": "error"
},
{
"pattern": [{}],
"inputs": ["https://example.com/"],
"expected_match": {
"protocol": { "input": "https", "groups": { "0": "https" }},
"hostname": { "input": "example.com", "groups": { "0": "example.com" }},
"pathname": { "input": "/", "groups": { "0": "/" }}
}
},
{
"pattern": [],
"inputs": ["https://example.com/"],
"expected_match": {
"protocol": { "input": "https", "groups": { "0": "https" }},
"hostname": { "input": "example.com", "groups": { "0": "example.com" }},
"pathname": { "input": "/", "groups": { "0": "/" }}
}
},
{
"pattern": [],
"inputs": [{}],
"expected_match": {}
},
{
"pattern": [],
"inputs": [],
"expected_match": { "inputs": [{}] }
},
{
"pattern": [{ "pathname": "(foo)(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }}
}
},
{
"pattern": [{ "pathname": "{(foo)bar}(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "baz" }}
}
},
{
"pattern": [{ "pathname": "(foo)?(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": "(foo)?*"
},
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}(barbaz)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "barbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}{(.*)}" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": "{:foo}(.*)"
},
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}{(.*)bar}" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": ":foo{*bar}"
},
"expected_match": null
},
{
"pattern": [{ "pathname": "{:foo}{bar(.*)}" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": ":foo{bar*}"
},
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "baz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}:bar(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": ":foo:bar(.*)"
},
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "f", "bar": "oobarbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo}?(.*)" }],
"inputs": [{ "pathname": "foobarbaz" }],
"expected_obj": {
"pathname": ":foo?*"
},
"expected_match": {
"pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }}
}
},
{
"pattern": [{ "pathname": "{:foo\\bar}" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": "{:foo\\.bar}" }],
"inputs": [{ "pathname": "foo.bar" }],
"expected_obj": {
"pathname": "{:foo.bar}"
},
"expected_match": {
"pathname": { "input": "foo.bar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": "{:foo(foo)bar}" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": "{:foo}bar" }],
"inputs": [{ "pathname": "foobar" }],
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": ":foo\\bar" }],
"inputs": [{ "pathname": "foobar" }],
"expected_obj": {
"pathname": "{:foo}bar"
},
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": ":foo{}(.*)" }],
"inputs": [{ "pathname": "foobar" }],
"expected_obj": {
"pathname": "{:foo}(.*)"
},
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "f", "0": "oobar" }}
}
},
{
"pattern": [{ "pathname": ":foo{}bar" }],
"inputs": [{ "pathname": "foobar" }],
"expected_obj": {
"pathname": "{:foo}bar"
},
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": ":foo{}?bar" }],
"inputs": [{ "pathname": "foobar" }],
"expected_obj": {
"pathname": "{:foo}bar"
},
"expected_match": {
"pathname": { "input": "foobar", "groups": { "foo": "foo" }}
}
},
{
"pattern": [{ "pathname": "*{}**?" }],
"inputs": [{ "pathname": "foobar" }],
"expected_obj": {
"pathname": "*(.*)?"
},
"//": "The `null` below is translated to undefined in the test harness.",
"expected_match": {
"pathname": { "input": "foobar", "groups": { "0": "foobar", "1": null }}
}
},
{
"pattern": [{ "pathname": ":foo(baz)(.*)" }],
"inputs": [{ "pathname": "bazbar" }],
"expected_match": {
"pathname": { "input": "bazbar", "groups": { "foo": "baz", "0": "bar" }}
}
},
{
"pattern": [{ "pathname": ":foo(baz)bar" }],
"inputs": [{ "pathname": "bazbar" }],
"expected_match": {
"pathname": { "input": "bazbar", "groups": { "foo": "baz" }}
}
},
{
"pattern": [{ "pathname": "*/*" }],
"inputs": [{ "pathname": "foo/bar" }],
"expected_match": {
"pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }}
}
},
{
"pattern": [{ "pathname": "*\\/*" }],
"inputs": [{ "pathname": "foo/bar" }],
"expected_obj": {
"pathname": "*/{*}"
},
"expected_match": {
"pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }}
}
},
{
"pattern": [{ "pathname": "*/{*}" }],
"inputs": [{ "pathname": "foo/bar" }],
"expected_match": {
"pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }}
}
},
{
"pattern": [{ "pathname": "*//*" }],
"inputs": [{ "pathname": "foo/bar" }],
"expected_match": null
},
{
"pattern": [{ "pathname": "/:foo." }],
"inputs": [{ "pathname": "/bar." }],
"expected_match": {
"pathname": { "input": "/bar.", "groups": { "foo": "bar" } }
}
},
{
"pattern": [{ "pathname": "/:foo.." }],
"inputs": [{ "pathname": "/bar.." }],
"expected_match": {
"pathname": { "input": "/bar..", "groups": { "foo": "bar" } }
}
},
{
"pattern": [{ "pathname": "./foo" }],
"inputs": [{ "pathname": "./foo" }],
"expected_match": {
"pathname": { "input": "./foo", "groups": {}}
}
},
{
"pattern": [{ "pathname": "../foo" }],
"inputs": [{ "pathname": "../foo" }],
"expected_match": {
"pathname": { "input": "../foo", "groups": {}}
}
},
{
"pattern": [{ "pathname": ":foo./" }],
"inputs": [{ "pathname": "bar./" }],
"expected_match": {
"pathname": { "input": "bar./", "groups": { "foo": "bar" }}
}
},
{
"pattern": [{ "pathname": ":foo../" }],
"inputs": [{ "pathname": "bar../" }],
"expected_match": {
"pathname": { "input": "bar../", "groups": { "foo": "bar" }}
}
},
{
"pattern": [{ "pathname": "/:foo\\bar" }],
"inputs": [{ "pathname": "/bazbar" }],
"expected_obj": {
"pathname": "{/:foo}bar"
},
"expected_match": {
"pathname": { "input": "/bazbar", "groups": { "foo": "baz" }}
}
},
{
"pattern": [{ "pathname": "/foo/bar" }, { "ignoreCase": true }],
"inputs": [{ "pathname": "/FOO/BAR" }],
"expected_match": {
"pathname": { "input": "/FOO/BAR", "groups": {} }
}
},
{
"pattern": [{ "ignoreCase": true }],
"inputs": [{ "pathname": "/FOO/BAR" }],
"expected_match": {
"pathname": { "input": "/FOO/BAR", "groups": { "0": "/FOO/BAR" } }
}
},
{
"pattern": [ "https://example.com:8080/foo?bar#baz",
{ "ignoreCase": true }],
"inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ",
"baseURL": "https://example.com:8080" }],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"port": "8080",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/FOO", "groups": {} },
"search": { "input": "BAR", "groups": {} },
"hash": { "input": "BAZ", "groups": {} }
}
},
{
"pattern": [ "/foo?bar#baz", "https://example.com:8080",
{ "ignoreCase": true }],
"inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ",
"baseURL": "https://example.com:8080" }],
"expected_obj": {
"protocol": "https",
"hostname": "example.com",
"port": "8080",
"pathname": "/foo",
"search": "bar",
"hash": "baz"
},
"expected_match": {
"protocol": { "input": "https", "groups": {} },
"hostname": { "input": "example.com", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/FOO", "groups": {} },
"search": { "input": "BAR", "groups": {} },
"hash": { "input": "BAZ", "groups": {} }
}
},
{
"pattern": [ "/foo?bar#baz", { "ignoreCase": true },
"https://example.com:8080" ],
"inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ",
"baseURL": "https://example.com:8080" }],
"expected_obj": "error"
},
{
"pattern": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }],
"inputs": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"pathname": "/a/\\+/b"
},
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/a/+/b", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "foo", "groups": {} }
}
},
{
"pattern": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }],
"inputs": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)"
},
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} },
"hash": { "input": "foo", "groups": {} }
}
},
{
"pattern": [ "#foo", "https://example.com/?q=*&v=?&hmm={}&umm=()" ],
"inputs": [ "https://example.com/?q=*&v=?&hmm={}&umm=()#foo" ],
"exactly_empty_components": [ "port" ],
"expected_obj": {
"search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)",
"hash": "foo"
},
"expected_match": {
"hostname": { "input": "example.com", "groups": {} },
"pathname": { "input": "/", "groups": {} },
"protocol": { "input": "https", "groups": {} },
"search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} },
"hash": { "input": "foo", "groups": {} }
}
}
]
================================================
FILE: test/urlpatterntests.js
================================================
import test from "ava";
import { readFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import "urlpattern-polyfill";
const kComponents = [
'protocol',
'username',
'password',
'hostname',
'port',
'password',
'pathname',
'search',
'hash',
];
function runTests(data) {
let i = 0;
for (let entry of data) {
test(`Pattern: ${JSON.stringify(entry.pattern)} ` +
`Inputs: ${JSON.stringify(entry.inputs)}`, t => {
const assert_throws_js = (type, fn, msg) => t.throws(fn, {instanceOf: type}, msg);
const assert_equals = (actual, expected, msg) => t.is(actual, expected, msg);
const assert_object_equals = (actual, expected, msg) => t.deepEqual(actual, expected, msg);
if (entry.expected_obj === 'error') {
assert_throws_js(TypeError, _ => new URLPattern(...entry.pattern),
'URLPattern() constructor');
return;
}
const pattern = new URLPattern(...entry.pattern);
// If the expected_obj property is not present we will automatically
// fill it with the most likely expected values.
entry.expected_obj = entry.expected_obj || {};
// The compiled URLPattern object should have a property for each
// component exposing the compiled pattern string.
for (let component of kComponents) {
// If the test case explicitly provides an expected pattern string,
// then use that. This is necessary in cases where the original
// construction pattern gets canonicalized, etc.
let expected = entry.expected_obj[component];
// If there is no explicit expected pattern string, then compute
// the expected value based on the URLPattern constructor args.
if (expected == undefined) {
// First determine if there is a baseURL present in the pattern
// input. A baseURL can be the source for many component patterns.
let baseURL = null;
if (entry.pattern.length > 0 && entry.pattern[0].baseURL) {
baseURL = new URL(entry.pattern[0].baseURL);
} else if (entry.pattern.length > 1 &&
typeof entry.pattern[1] === 'string') {
baseURL = new URL(entry.pattern[1]);
}
const EARLIER_COMPONENTS = {
protocol: [],
hostname: ["protocol"],
port: ["protocol", "hostname"],
username: [],
password: [],
pathname: ["protocol", "hostname", "port"],
search: ["protocol", "hostname", "port", "pathname"],
hash: ["protocol", "hostname", "port", "pathname", "search"],
};
// We automatically populate the expected pattern string using
// the following options in priority order:
//
// 1. If the original input explicitly provided a pattern, then
// echo that back as the expected value.
// 2. If an "earlier" component is specified, then a wildcard
// will be used rather than inheriting from the base URL.
// 3. If the baseURL exists and provides a component value then
// use that for the expected pattern.
// 4. Otherwise fall back on the default pattern of `*` for an
// empty component pattern.
//
// Note that username and password are never inherited, and will only
// need to match if explicitly specified.
if (entry.exactly_empty_components &&
entry.exactly_empty_components.includes(component)) {
expected = '';
} else if (typeof entry.pattern[0] === 'object' &&
entry.pattern[0][component]) {
expected = entry.pattern[0][component];
} else if (typeof entry.pattern[0] === 'object' &&
EARLIER_COMPONENTS[component].some(c => c in entry.pattern[0])) {
expected = '*';
} else if (baseURL && component !== 'username' && component !== 'password') {
let base_value = baseURL[component];
// Unfortunately some URL() getters include separator chars; e.g.
// the trailing `:` for the protocol. Strip those off if necessary.
if (component === 'protocol')
base_value = base_value.substring(0, base_value.length - 1);
else if (component === 'search' || component === 'hash')
base_value = base_value.substring(1, base_value.length);
expected = base_value;
} else {
expected = '*';
}
}
// Finally, assert that the compiled object property matches the
// expected property.
assert_equals(pattern[component], expected,
`compiled pattern property '${component}'`);
}
if (entry.expected_match === 'error') {
assert_throws_js(TypeError, _ => pattern.test(...entry.inputs),
'test() result');
assert_throws_js(TypeError, _ => pattern.exec(...entry.inputs),
'exec() result');
return;
}
// First, validate the test() method by converting the expected result to
// a truthy value.
assert_equals(pattern.test(...entry.inputs), !!entry.expected_match,
'test() result');
// Next, start validating the exec() method.
const exec_result = pattern.exec(...entry.inputs);
// On a failed match exec() returns null.
if (!entry.expected_match || typeof entry.expected_match !== "object") {
assert_equals(exec_result, entry.expected_match, 'exec() failed match result');
return;
}
if (!entry.expected_match.inputs)
entry.expected_match.inputs = entry.inputs;
// Next verify the result.input is correct. This may be a structured
// URLPatternInit dictionary object or a URL string.
assert_equals(exec_result.inputs.length,
entry.expected_match.inputs.length,
'exec() result.inputs.length');
for (let i = 0; i < exec_result.inputs.length; ++i) {
const input = exec_result.inputs[i];
const expected_input = entry.expected_match.inputs[i];
if (typeof input === 'string') {
assert_equals(input, expected_input, `exec() result.inputs[${i}]`);
continue;
}
for (let component of kComponents) {
assert_equals(input[component], expected_input[component],
`exec() result.inputs[${i}][${component}]`);
}
}
// Next we will compare the URLPatternComponentResult for each of these
// expected components.
for (let component of kComponents) {
let expected_obj = entry.expected_match[component];
// If the test expectations don't include a component object, then
// we auto-generate one. This is convenient for the many cases
// where the pattern has a default wildcard or empty string pattern
// for a component and the input is essentially empty.
if (!expected_obj) {
expected_obj = { input: '', groups: {} };
// Next, we must treat default wildcards differently than empty string
// patterns. The wildcard results in a capture group, but the empty
// string pattern does not. The expectation object must list which
// components should be empty instead of wildcards in
// |exactly_empty_components|.
if (!entry.exactly_empty_components ||
!entry.exactly_empty_components.includes(component)) {
expected_obj.groups['0'] = '';
}
}
// JSON does not allow us to use undefined directly, so the data file
// contains null instead. Translate to the expected undefined value
// here.
for (const key in expected_obj.groups) {
if (expected_obj.groups[key] === null) {
expected_obj.groups[key] = undefined;
}
}
assert_object_equals(exec_result[component], expected_obj,
`exec() result for ${component}`);
}
});
}
}
let path = fileURLToPath(import.meta.url);
let data = readFileSync(resolve(dirname(path), "urlpatterntestdata.json"));
runTests(JSON.parse(data));
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"lib": ["es2020", "dom"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": true,
"outDir": "./dist",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts"],
"exclude": []
}
================================================
FILE: urlpattern/package.json
================================================
{
"main": "../dist/urlpattern.cjs",
"module": "../dist/urlpattern.js",
"types": "../dist/types.d.ts"
}