import { registerUncloneable } from "ext:web/serialization.js"; import { ObjectCreate, ObjectDefineProperty, RegExp, RegExpPrototypeExec, TypeError, WeakMap, WeakMapPrototypeGet, WeakMapPrototypeSet, } from "ext:core/primordials"; import { isObject, usv } from "ext:web/idl.js"; import { installInterface } from "ext:web/interface.js"; import { op_urlpattern_match_input, op_urlpattern_parse } from "ext:core/ops"; import type { NativeComponent, PatternComponent, PatternInit, PatternInput } from "ext:core/ops"; interface Component { pattern: string; regexp: RegExp; names: string[]; } interface PatternState { components: Component[]; hasRegExpGroups: boolean; } type PatternResult = Record; input: string; }> & { inputs: [PatternInput] | [PatternInput, string]; }; const patterns = new WeakMap(); registerUncloneable(patterns); // Order matches the native parser's component array; dictionaries convert in lexicographic order. const components: PatternComponent[] = [ "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash", ]; const members: (PatternComponent | "baseURL")[] = [ "baseURL", "hash", "hostname", "password", "pathname", "port", "protocol", "search", "username", ]; function convertInput(value: unknown): PatternInput { if (value !== null && value !== undefined && !isObject(value)) { return usv(value); } const input: PatternInit = ObjectCreate(null); if (value !== null && value !== undefined) { for (let index = 0; index < members.length; index++) { const name = members[index]; const member = value[name]; if (member !== undefined) input[name] = usv(member); } } return input; } function state(receiver: unknown): PatternState { const value = WeakMapPrototypeGet(patterns, receiver); if (!value) throw new TypeError("Illegal invocation"); return value; } function define(object: T, name: K, value: NoInfer): void { ObjectDefineProperty(object, name, { value, writable: true, enumerable: true, configurable: true, }); } function match(receiver: unknown, input: unknown, base: unknown, exec: false): boolean; function match(receiver: unknown, input: unknown, base: unknown, exec: true): PatternResult | null; function match(receiver: unknown, rawInput: unknown, rawBase: unknown, exec: boolean): PatternResult | null | boolean { const data = state(receiver); let input = convertInput(rawInput); const base = rawBase === undefined ? undefined : usv(rawBase); const parsed = op_urlpattern_match_input([input, base ?? null]); if (parsed[1] !== null) throw new TypeError(parsed[1]); const values = parsed[0]; if (values === null) return exec ? null : false; const result: Partial | null = exec ? { hash: undefined, hostname: undefined, inputs: undefined, password: undefined, pathname: undefined, port: undefined, protocol: undefined, search: undefined, username: undefined, } : null; for (let index = 0; index < components.length; index++) { const name = components[index]; const component = data.components[index]; const captures = RegExpPrototypeExec(component.regexp, values[name]); if (captures === null) return exec ? null : false; if (exec) { const groups: Record = {}; for (let group = 0; group < component.names.length; group++) { define(groups, component.names[group], captures[group + 1]); } define(result!, name, { groups, input: values[name] }); } } if (!exec) return true; if (typeof input !== "string") { const dictionary: PatternInit = {}; for (let index = 0; index < members.length; index++) { const name = members[index]; if (input[name] !== undefined) define(dictionary, name, input[name]); } input = dictionary; } result!.inputs = base === undefined ? [input] : [input, base]; // Every component was defined above; partial results never escape. return result as PatternResult; } interface URLPattern extends Readonly> {} class URLPattern { constructor( rawInput: unknown = undefined, baseOrOptions: unknown = undefined, options: unknown = undefined, ) { const withBase = arguments.length >= 3 || (baseOrOptions !== null && baseOrOptions !== undefined && !isObject(baseOrOptions)); const input = convertInput(rawInput); const base = withBase ? usv(baseOrOptions) : null; options = withBase ? options : baseOrOptions; if (options !== null && options !== undefined && !isObject(options)) { throw new TypeError("Invalid URLPattern options"); } const ignoreCase = !!options?.ignoreCase; const parsed = op_urlpattern_parse([input, base, ignoreCase]); if (parsed[1] !== null) throw new TypeError(parsed[1]); // Reuse the native array: new indexed writes could hit inherited setters. const compiled: (NativeComponent | Component)[] = parsed[0][0]; for (let index = 0; index < compiled.length; index++) { const component = compiled[index] as NativeComponent; compiled[index] = { pattern: component[0], regexp: new RegExp(component[1], component[3]), names: component[2], }; } WeakMapPrototypeSet(patterns, this, { components: compiled as Component[], hasRegExpGroups: parsed[0][1], }); } test(input: unknown = undefined, base: unknown = undefined): boolean { return match(this, input, base, false); } exec(input: unknown = undefined, base: unknown = undefined): PatternResult | null { return match(this, input, base, true); } get hasRegExpGroups(): boolean { return state(this).hasRegExpGroups; } } for (let index = 0; index < components.length; index++) { const name = components[index]; const getter = { get(this: unknown): string { return state(this).components[index].pattern; }, }.get; ObjectDefineProperty(getter, "name", { value: `get ${name}`, configurable: true, }); ObjectDefineProperty(URLPattern.prototype, name, { get: getter, enumerable: true, configurable: true, }); } installInterface("URLPattern", URLPattern); export { URLPattern };