import { Array, ArrayPrototypePush, ArrayPrototypeSort as sort, ArrayPrototypeSplice as splice, IteratorPrototype, Map, MapPrototypeGet, MapPrototypeHas, MapPrototypeSet, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, ObjectSetPrototypeOf, ReflectApply, ReflectOwnKeys, StringPrototypeSlice, SymbolIterator, SymbolToStringTag, TypeError, WeakMapPrototypeSet as weakSet, } from "ext:core/primordials"; import { isObject, required, state, usv } from "ext:web/idl.js"; import { markUncloneable } from "ext:web/serialization.js"; import { op_url_search_parse, op_url_search_serialize } from "ext:core/ops"; import { params, privateArray, update } from "./state.js"; import type { SearchPair } from "ext:core/ops"; import type { ParamsState } from "./state.js"; type IteratorValues = { 0: SearchPair; 1: string; 2: string }; const arrayPrototype = Array.prototype; function iterable(value: object, method: Function): Iterable { return { [SymbolIterator]() { // for-of validates the returned iterator and owns IteratorClose. return ReflectApply(method, value, []) as Iterator; }, }; } interface ParamsIterator extends IterableIterator {} class ParamsIterator { #state: ParamsState; #index = 0; #kind: K; constructor(value: ParamsState, kind: K) { markUncloneable(this); this.#state = value; this.#kind = kind; } next(): IteratorResult { const value = this.#state; if (this.#index >= value.list.length) { return { value: undefined, done: true }; } const pair = value.list[this.#index++]; return { value: (this.#kind === 0 ? [pair[0], pair[1]] : pair[this.#kind - 1]) as IteratorValues[K], done: false, }; } } delete (ParamsIterator.prototype as { constructor?: Function }).constructor; ObjectDefineProperty(ParamsIterator.prototype, "next", { enumerable: true }); ObjectSetPrototypeOf(ParamsIterator.prototype, IteratorPrototype); ObjectDefineProperty(ParamsIterator.prototype, SymbolToStringTag, { value: "URLSearchParams Iterator", configurable: true, }); interface URLSearchParams { [Symbol.iterator](): ParamsIterator<0>; } class URLSearchParams { constructor(init: unknown = "") { const list = privateArray(); if (isObject(init)) { const method = init[SymbolIterator]; if (method !== undefined && method !== null) { if (typeof method !== "function") { throw new TypeError("Invalid iterator"); } for (const pair of iterable(init, method)) { if (!isObject(pair)) { throw new TypeError("Query pairs must be sequences"); } const method = pair[SymbolIterator]; if (typeof method !== "function") { throw new TypeError("Query pairs must be sequences"); } const values = privateArray(); for (const item of iterable(pair, method)) { ArrayPrototypePush(values, usv(item)); } ArrayPrototypePush(list, values); } for (let index = 0; index < list.length; index++) { if (list[index].length !== 2) { throw new TypeError("Query pairs must have exactly two items"); } } } else { const keys = new Map(); const ownKeys = ReflectOwnKeys(init); for (let index = 0; index < ownKeys.length; index++) { const key = ownKeys[index]; if (!ObjectGetOwnPropertyDescriptor(init, key)?.enumerable) continue; const name = usv(key); const value = usv(init[key]); if (MapPrototypeHas(keys, name)) { list[MapPrototypeGet(keys, name)!][1] = value; } else { MapPrototypeSet(keys, name, list.length); ArrayPrototypePush(list, privateArray([name, value])); } } } } else { const text = usv(init); const parsed = op_url_search_parse( text[0] === "?" ? StringPrototypeSlice(text, 1) : text, ); for (let index = 0; index < parsed.length; index++) { ArrayPrototypePush(list, privateArray(parsed[index])); } } // Pair lengths are checked only after the entire sequence has been converted. weakSet(params, this, { list: list as SearchPair[], url: null }); } get size(): number { return state(params, this).list.length; } append(rawName: unknown, rawValue: unknown): void { const data = state(params, this); required(arguments.length, 2); const name = usv(rawName); const value = usv(rawValue); ArrayPrototypePush(data.list, privateArray([name, value])); update(data); } delete(rawName: unknown, rawValue: unknown = undefined): void { const data = state(params, this); required(arguments.length, 1); const name = usv(rawName); const value = rawValue === undefined ? undefined : usv(rawValue); for (let index = 0; index < data.list.length;) { const pair = data.list[index]; if (pair[0] === name && (value === undefined || pair[1] === value)) { splice(data.list, index, 1); } else index++; } update(data); } get(rawName: unknown): string | null { const data = state(params, this); required(arguments.length, 1); const name = usv(rawName); for (let index = 0; index < data.list.length; index++) { if (data.list[index][0] === name) return data.list[index][1]; } return null; } getAll(rawName: unknown): string[] { const data = state(params, this); required(arguments.length, 1); const name = usv(rawName); const result = privateArray(); for (let index = 0; index < data.list.length; index++) { if (data.list[index][0] === name) { ArrayPrototypePush(result, data.list[index][1]); } } ObjectSetPrototypeOf(result, arrayPrototype); return result; } has(rawName: unknown, rawValue: unknown = undefined): boolean { const data = state(params, this); required(arguments.length, 1); const name = usv(rawName); const value = rawValue === undefined ? undefined : usv(rawValue); for (let index = 0; index < data.list.length; index++) { const pair = data.list[index]; if (pair[0] === name && (value === undefined || pair[1] === value)) { return true; } } return false; } set(rawName: unknown, rawValue: unknown): void { const data = state(params, this); required(arguments.length, 2); const name = usv(rawName); const value = usv(rawValue); let found = false; for (let index = 0; index < data.list.length;) { if (data.list[index][0] !== name) index++; else if (found) splice(data.list, index, 1); else { data.list[index++][1] = value; found = true; } } if (!found) ArrayPrototypePush(data.list, privateArray([name, value])); update(data); } sort(): void { const data = state(params, this); // JS string comparison supplies the required UTF-16 ordering, not UTF-8 ordering. sort(data.list, (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0); update(data); } forEach(callback: (value: string, name: string, parent: URLSearchParams) => void, thisArg: unknown = undefined): void { const data = state(params, this); required(arguments.length, 1); if (typeof callback !== "function") { throw new TypeError("Callback must be callable"); } for (let index = 0; index < data.list.length; index++) { const pair = data.list[index]; ReflectApply(callback, thisArg, [pair[1], pair[0], this]); } } entries(): ParamsIterator<0> { return new ParamsIterator(state(params, this), 0); } keys(): ParamsIterator<1> { return new ParamsIterator(state(params, this), 1); } values(): ParamsIterator<2> { return new ParamsIterator(state(params, this), 2); } toString(): string { return op_url_search_serialize(state(params, this).list); } } export { URLSearchParams };