1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 16:19:12 -05:00

feat: add URLPattern API (#11941)

This adds support for the URLPattern  API.

The API is added in --unstable only, as it has not yet shipped in any
browser. It is targeted for shipping in Chrome 95.

Spec: https://wicg.github.io/urlpattern/

Co-authored-by: crowlKats < crowlkats@toaxl.com >
This commit is contained in:
Luca Casonato 2021-09-08 11:14:29 +02:00 committed by GitHub
parent 2de5587547
commit e07f28d301
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 589 additions and 3 deletions

55
Cargo.lock generated
View file

@ -923,6 +923,7 @@ dependencies = [
"percent-encoding",
"serde",
"serde_repr",
"urlpattern",
]
[[package]]
@ -4223,6 +4224,47 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.6.0"
@ -4290,6 +4332,19 @@ dependencies = [
"serde",
]
[[package]]
name = "urlpattern"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe1da4e25c8758a07ac5b97fe72dec49416ea0783bfa9d6c24793c3a34f1e4e"
dependencies = [
"derive_more",
"regex",
"serde",
"unic-ucd-ident",
"url",
]
[[package]]
name = "utf-8"
version = "0.7.6"

View file

@ -0,0 +1,45 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals, unitTest } from "./test_util.ts";
unitTest(function urlPatternFromString() {
const pattern = new URLPattern("https://deno.land/foo/:bar");
assertEquals(pattern.protocol, "https");
assertEquals(pattern.hostname, "deno.land");
assertEquals(pattern.pathname, "/foo/:bar");
assert(pattern.test("https://deno.land/foo/x"));
assert(!pattern.test("https://deno.com/foo/x"));
const match = pattern.exec("https://deno.land/foo/x");
assert(match);
assertEquals(match.pathname.input, "/foo/x");
assertEquals(match.pathname.groups, { bar: "x" });
});
unitTest(function urlPatternFromStringWithBase() {
const pattern = new URLPattern("/foo/:bar", "https://deno.land");
assertEquals(pattern.protocol, "https");
assertEquals(pattern.hostname, "deno.land");
assertEquals(pattern.pathname, "/foo/:bar");
assert(pattern.test("https://deno.land/foo/x"));
assert(!pattern.test("https://deno.com/foo/x"));
const match = pattern.exec("https://deno.land/foo/x");
assert(match);
assertEquals(match.pathname.input, "/foo/x");
assertEquals(match.pathname.groups, { bar: "x" });
});
unitTest(function urlPatternFromInit() {
const pattern = new URLPattern({
pathname: "/foo/:bar",
});
assertEquals(pattern.protocol, "*");
assertEquals(pattern.hostname, "*");
assertEquals(pattern.pathname, "/foo/:bar");
assert(pattern.test("https://deno.land/foo/x"));
assert(pattern.test("https://deno.com/foo/x"));
assert(!pattern.test("https://deno.com/bar/x"));
assert(pattern.test({ pathname: "/foo/x" }));
});

269
ext/url/01_urlpattern.js Normal file
View file

@ -0,0 +1,269 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../../core/internal.d.ts" />
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../webidl/internal.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="./lib.deno_url.d.ts" />
"use strict";
((window) => {
const core = window.Deno.core;
const webidl = window.__bootstrap.webidl;
const {
ArrayPrototypeMap,
ObjectKeys,
ObjectFromEntries,
RegExp,
RegExpPrototypeExec,
RegExpPrototypeTest,
Symbol,
SymbolFor,
TypeError,
} = window.__bootstrap.primordials;
const _components = Symbol("components");
/**
* @typedef Components
* @property {Component} protocol
* @property {Component} username
* @property {Component} password
* @property {Component} hostname
* @property {Component} port
* @property {Component} pathname
* @property {Component} search
* @property {Component} hash
*/
/**
* @typedef Component
* @property {string} patternString
* @property {RegExp} regexp
* @property {string[]} groupNameList
*/
class URLPattern {
/** @type {Components} */
[_components];
/**
* @param {URLPatternInput} input
* @param {string} [baseURL]
*/
constructor(input, baseURL = undefined) {
this[webidl.brand] = webidl.brand;
const prefix = "Failed to construct 'URLPattern'";
webidl.requiredArguments(arguments.length, 1, { prefix });
input = webidl.converters.URLPatternInput(input, {
prefix,
context: "Argument 1",
});
if (baseURL !== undefined) {
baseURL = webidl.converters.USVString(baseURL, {
prefix,
context: "Argument 2",
});
}
const components = core.opSync("op_urlpattern_parse", input, baseURL);
for (const key of ObjectKeys(components)) {
try {
components[key].regexp = new RegExp(
components[key].regexpString,
"u",
);
} catch (e) {
throw new TypeError(`${prefix}: ${key} is invalid; ${e.message}`);
}
}
this[_components] = components;
}
get protocol() {
webidl.assertBranded(this, URLPattern);
return this[_components].protocol.patternString;
}
get username() {
webidl.assertBranded(this, URLPattern);
return this[_components].username.patternString;
}
get password() {
webidl.assertBranded(this, URLPattern);
return this[_components].password.patternString;
}
get hostname() {
webidl.assertBranded(this, URLPattern);
return this[_components].hostname.patternString;
}
get port() {
webidl.assertBranded(this, URLPattern);
return this[_components].port.patternString;
}
get pathname() {
webidl.assertBranded(this, URLPattern);
return this[_components].pathname.patternString;
}
get search() {
webidl.assertBranded(this, URLPattern);
return this[_components].search.patternString;
}
get hash() {
webidl.assertBranded(this, URLPattern);
return this[_components].hash.patternString;
}
/**
* @param {URLPatternInput} input
* @param {string} [baseURL]
* @returns {boolean}
*/
test(input, baseURL = undefined) {
webidl.assertBranded(this, URLPattern);
const prefix = "Failed to execute 'test' on 'URLPattern'";
webidl.requiredArguments(arguments.length, 1, { prefix });
input = webidl.converters.URLPatternInput(input, {
prefix,
context: "Argument 1",
});
if (baseURL !== undefined) {
baseURL = webidl.converters.USVString(baseURL, {
prefix,
context: "Argument 2",
});
}
const res = core.opSync(
"op_urlpattern_process_match_input",
input,
baseURL,
);
if (res === null) {
return false;
}
const [values] = res;
for (const key of ObjectKeys(values)) {
if (!RegExpPrototypeTest(this[_components][key].regexp, values[key])) {
return false;
}
}
return true;
}
/**
* @param {URLPatternInput} input
* @param {string} [baseURL]
* @returns {URLPatternResult | null}
*/
exec(input, baseURL = undefined) {
webidl.assertBranded(this, URLPattern);
const prefix = "Failed to execute 'exec' on 'URLPattern'";
webidl.requiredArguments(arguments.length, 1, { prefix });
input = webidl.converters.URLPatternInput(input, {
prefix,
context: "Argument 1",
});
if (baseURL !== undefined) {
baseURL = webidl.converters.USVString(baseURL, {
prefix,
context: "Argument 2",
});
}
const res = core.opSync(
"op_urlpattern_process_match_input",
input,
baseURL,
);
if (res === null) {
return null;
}
const [values, inputs] = res;
if (inputs[1] === null) {
inputs.pop();
}
/** @type {URLPatternResult} */
const result = { inputs };
/** @type {string} */
for (const key of ObjectKeys(values)) {
/** @type {Component} */
const component = this[_components][key];
const input = values[key];
const match = RegExpPrototypeExec(component.regexp, input);
if (match === null) {
return null;
}
const groupEntries = ArrayPrototypeMap(
component.groupNameList,
(name, i) => [name, match[i + 1] ?? ""],
);
const groups = ObjectFromEntries(groupEntries);
result[key] = {
input,
groups,
};
}
return result;
}
[SymbolFor("Deno.customInspect")](inspect) {
return `URLPattern ${
inspect({
protocol: this.protocol,
username: this.username,
password: this.password,
hostname: this.hostname,
port: this.port,
pathname: this.pathname,
search: this.search,
hash: this.hash,
})
}`;
}
}
webidl.configurePrototype(URLPattern);
webidl.converters.URLPatternInit = webidl
.createDictionaryConverter("URLPatternInit", [
{ key: "protocol", converter: webidl.converters.USVString },
{ key: "username", converter: webidl.converters.USVString },
{ key: "password", converter: webidl.converters.USVString },
{ key: "hostname", converter: webidl.converters.USVString },
{ key: "port", converter: webidl.converters.USVString },
{ key: "pathname", converter: webidl.converters.USVString },
{ key: "search", converter: webidl.converters.USVString },
{ key: "hash", converter: webidl.converters.USVString },
{ key: "baseURL", converter: webidl.converters.USVString },
]);
webidl.converters["URLPatternInput"] = (V, opts) => {
// Union for (URLPatternInit or USVString)
if (typeof V == "object") {
return webidl.converters.URLPatternInit(V, opts);
}
return webidl.converters.USVString(V, opts);
};
window.__bootstrap.urlPattern = {
URLPattern,
};
})(globalThis);

View file

@ -19,6 +19,7 @@ idna = "0.2.3"
percent-encoding = "2.1.0"
serde = { version = "1.0.129", features = ["derive"] }
serde_repr = "0.1.7"
urlpattern = "0.1.2"
[dev-dependencies]
deno_bench_util = { version = "0.10.0", path = "../../bench_util" }

View file

@ -1,5 +1,6 @@
# deno_url
This crate implements the URL API for Deno.
This crate implements the URL, and URLPattern APIs for Deno.
Spec: https://url.spec.whatwg.org/
URL Spec: https://url.spec.whatwg.org/ URLPattern Spec:
https://wicg.github.io/urlpattern/

View file

@ -10,5 +10,9 @@ declare namespace globalThis {
URLSearchParams: typeof URLSearchParams;
parseUrlEncoded(bytes: Uint8Array): [string, string][];
};
declare var urlPattern: {
URLPattern: typeof URLPattern;
};
}
}

View file

@ -172,3 +172,139 @@ declare class URL {
username: string;
toJSON(): string;
}
declare interface URLPatternInit {
protocol?: string;
username?: string;
password?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
hash?: string;
baseURL?: string;
}
declare type URLPatternInput = string | URLPatternInit;
declare interface URLPatternComponentResult {
input: string;
groups: Record<string, string>;
}
/** `URLPatternResult` is the object returned from `URLPattern.match`. */
declare interface URLPatternResult {
/** The inputs provided when matching. */
inputs: [URLPatternInit] | [URLPatternInit, string];
/** The matched result for the `protocol` matcher. */
protocol: URLPatternComponentResult;
/** The matched result for the `username` matcher. */
username: URLPatternComponentResult;
/** The matched result for the `password` matcher. */
password: URLPatternComponentResult;
/** The matched result for the `hostname` matcher. */
hostname: URLPatternComponentResult;
/** The matched result for the `port` matcher. */
port: URLPatternComponentResult;
/** The matched result for the `pathname` matcher. */
pathname: URLPatternComponentResult;
/** The matched result for the `search` matcher. */
search: URLPatternComponentResult;
/** The matched result for the `hash` matcher. */
hash: URLPatternComponentResult;
}
/**
* The URLPattern API provides a web platform primitive for matching URLs based
* on a convenient pattern syntax.
*
* The syntax is based on path-to-regexp. Wildcards, named capture groups,
* regular groups, and group modifiers are all supported.
*
* ```ts
* // Specify the pattern as structured data.
* const pattern = new URLPattern({ pathname: "/users/:user" });
* const match = pattern.match("/users/joe");
* console.log(match.pathname.groups.user); // joe
* ```
*
* ```ts
* // Specify a fully qualified string pattern.
* const pattern = new URLPattern("https://example.com/books/:id");
* console.log(pattern.test("https://example.com/books/123")); // true
* console.log(pattern.test("https://deno.land/books/123")); // false
* ```
*
* ```ts
* // Specify a relative string pattern with a base URL.
* const pattern = new URLPattern("/:article", "https://blog.example.com");
* console.log(pattern.test("https://blog.example.com/article")); // true
* console.log(pattern.test("https://blog.example.com/article/123")); // false
* ```
*/
declare class URLPattern {
constructor(input: URLPatternInput, baseURL?: string);
/**
* Test if the given input matches the stored pattern.
*
* The input can either be provided as a url string (with an optional base),
* or as individual components in the form of an object.
*
* ```ts
* const pattern = new URLPattern("https://example.com/books/:id");
*
* // Test a url string.
* console.log(pattern.test("https://example.com/books/123")); // true
*
* // Test a relative url with a base.
* console.log(pattern.test("/books/123", "https://example.com")); // true
*
* // Test an object of url components.
* console.log(pattern.test({ pathname: "/books/123" })); // true
* ```
*/
test(input: URLPatternInput, baseURL?: string): boolean;
/**
* Match the given input against the stored pattern.
*
* The input can either be provided as a url string (with an optional base),
* or as individual components in the form of an object.
*
* ```ts
* const pattern = new URLPattern("https://example.com/books/:id");
*
* // Match a url string.
* let match = pattern.match("https://example.com/books/123");
* console.log(match.pathname.groups.id); // 123
*
* // Match a relative url with a base.
* match = pattern.match("/books/123", "https://example.com");
* console.log(match.pathname.groups.id); // 123
*
* // Match an object of url components.
* match = pattern.match({ pathname: "/books/123" });
* console.log(match.pathname.groups.id); // 123
* ```
*/
exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null;
/** The pattern string for the `protocol`. */
readonly protocol: string;
/** The pattern string for the `username`. */
readonly username: string;
/** The pattern string for the `password`. */
readonly password: string;
/** The pattern string for the `hostname`. */
readonly hostname: string;
/** The pattern string for the `port`. */
readonly port: string;
/** The pattern string for the `pathname`. */
readonly pathname: string;
/** The pattern string for the `search`. */
readonly search: string;
/** The pattern string for the `hash`. */
readonly hash: string;
}

View file

@ -1,5 +1,7 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
mod urlpattern;
use deno_core::error::generic_error;
use deno_core::error::type_error;
use deno_core::error::uri_error;
@ -14,11 +16,15 @@ use deno_core::ZeroCopyBuf;
use std::panic::catch_unwind;
use std::path::PathBuf;
use crate::urlpattern::op_urlpattern_parse;
use crate::urlpattern::op_urlpattern_process_match_input;
pub fn init() -> Extension {
Extension::builder()
.js(include_js_files!(
prefix "deno:ext/url",
"00_url.js",
"01_urlpattern.js",
))
.ops(vec![
("op_url_parse", op_sync(op_url_parse)),
@ -31,6 +37,11 @@ pub fn init() -> Extension {
"op_url_stringify_search_params",
op_sync(op_url_stringify_search_params),
),
("op_urlpattern_parse", op_sync(op_urlpattern_parse)),
(
"op_urlpattern_process_match_input",
op_sync(op_urlpattern_process_match_input),
),
])
.build()
}

40
ext/url/urlpattern.rs Normal file
View file

@ -0,0 +1,40 @@
use deno_core::error::type_error;
use deno_core::error::AnyError;
use urlpattern::quirks;
use urlpattern::quirks::MatchInput;
use urlpattern::quirks::StringOrInit;
use urlpattern::quirks::UrlPattern;
pub fn op_urlpattern_parse(
_state: &mut deno_core::OpState,
input: StringOrInit,
base_url: Option<String>,
) -> Result<UrlPattern, AnyError> {
let init = urlpattern::quirks::process_construct_pattern_input(
input,
base_url.as_deref(),
)
.map_err(|e| type_error(e.to_string()))?;
let pattern = urlpattern::quirks::parse_pattern(init)
.map_err(|e| type_error(e.to_string()))?;
Ok(pattern)
}
pub fn op_urlpattern_process_match_input(
_state: &mut deno_core::OpState,
input: StringOrInit,
base_url: Option<String>,
) -> Result<Option<(MatchInput, quirks::Inputs)>, AnyError> {
let res = urlpattern::quirks::process_match_input(input, base_url.as_deref())
.map_err(|e| type_error(e.to_string()))?;
let (input, inputs) = match res {
Some((input, inputs)) => (input, inputs),
None => return Ok(None),
};
Ok(urlpattern::quirks::parse_match_input(input).map(|input| (input, inputs)))
}

View file

@ -41,6 +41,7 @@ delete Object.prototype.__proto__;
const performance = window.__bootstrap.performance;
const crypto = window.__bootstrap.crypto;
const url = window.__bootstrap.url;
const urlPattern = window.__bootstrap.urlPattern;
const headers = window.__bootstrap.headers;
const streams = window.__bootstrap.streams;
const fileReader = window.__bootstrap.fileReader;
@ -431,8 +432,9 @@ delete Object.prototype.__proto__;
};
const unstableWindowOrWorkerGlobalScope = {
WebSocketStream: util.nonEnumerable(webSocket.WebSocketStream),
BroadcastChannel: util.nonEnumerable(broadcastChannel.BroadcastChannel),
URLPattern: util.nonEnumerable(urlPattern.URLPattern),
WebSocketStream: util.nonEnumerable(webSocket.WebSocketStream),
GPU: util.nonEnumerable(webgpu.GPU),
GPUAdapter: util.nonEnumerable(webgpu.GPUAdapter),

View file

@ -13490,6 +13490,14 @@
],
"toString.tentative.any.html": false,
"type.tentative.any.html": false
},
"function": {
"call.tentative.any.html": false,
"constructor.tentative.any.html": [
"construct with JS function"
],
"table.tentative.any.html": false,
"type.tentative.any.html": false
}
},
"serialization": {
@ -14527,5 +14535,19 @@
"performance.clearResourceTimings in workers",
"performance.setResourceTimingBufferSize in workers"
]
},
"urlpattern": {
"urlpattern.any.html": [
"Pattern: [{\"pathname\":\"/foo/bar\"}] Inputs: [\"./foo/bar\",\"https://example.com\"]"
],
"urlpattern.any.worker.html": [
"Pattern: [{\"pathname\":\"/foo/bar\"}] Inputs: [\"./foo/bar\",\"https://example.com\"]"
],
"urlpattern.https.any.html": [
"Pattern: [{\"pathname\":\"/foo/bar\"}] Inputs: [\"./foo/bar\",\"https://example.com\"]"
],
"urlpattern.https.any.worker.html": [
"Pattern: [{\"pathname\":\"/foo/bar\"}] Inputs: [\"./foo/bar\",\"https://example.com\"]"
]
}
}