diff options
Diffstat (limited to 'ext')
-rw-r--r-- | ext/url/01_urlpattern.js | 269 | ||||
-rw-r--r-- | ext/url/Cargo.toml | 1 | ||||
-rw-r--r-- | ext/url/README.md | 5 | ||||
-rw-r--r-- | ext/url/internal.d.ts | 4 | ||||
-rw-r--r-- | ext/url/lib.deno_url.d.ts | 136 | ||||
-rw-r--r-- | ext/url/lib.rs | 11 | ||||
-rw-r--r-- | ext/url/urlpattern.rs | 40 |
7 files changed, 464 insertions, 2 deletions
diff --git a/ext/url/01_urlpattern.js b/ext/url/01_urlpattern.js new file mode 100644 index 000000000..b6ff9e40e --- /dev/null +++ b/ext/url/01_urlpattern.js @@ -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); diff --git a/ext/url/Cargo.toml b/ext/url/Cargo.toml index 830661015..8ad3a61f1 100644 --- a/ext/url/Cargo.toml +++ b/ext/url/Cargo.toml @@ -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" } diff --git a/ext/url/README.md b/ext/url/README.md index 991dd8b20..519c2823e 100644 --- a/ext/url/README.md +++ b/ext/url/README.md @@ -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/ diff --git a/ext/url/internal.d.ts b/ext/url/internal.d.ts index ec2c2688c..45bf670aa 100644 --- a/ext/url/internal.d.ts +++ b/ext/url/internal.d.ts @@ -10,5 +10,9 @@ declare namespace globalThis { URLSearchParams: typeof URLSearchParams; parseUrlEncoded(bytes: Uint8Array): [string, string][]; }; + + declare var urlPattern: { + URLPattern: typeof URLPattern; + }; } } diff --git a/ext/url/lib.deno_url.d.ts b/ext/url/lib.deno_url.d.ts index df7acbe60..b2ef4095f 100644 --- a/ext/url/lib.deno_url.d.ts +++ b/ext/url/lib.deno_url.d.ts @@ -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; +} diff --git a/ext/url/lib.rs b/ext/url/lib.rs index d8987c816..0f8d5c599 100644 --- a/ext/url/lib.rs +++ b/ext/url/lib.rs @@ -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() } diff --git a/ext/url/urlpattern.rs b/ext/url/urlpattern.rs new file mode 100644 index 000000000..b9f53665f --- /dev/null +++ b/ext/url/urlpattern.rs @@ -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))) +} |