import { markUncloneable, registerUncloneable } from "ext:web/serialization.js"; import { Array, ArrayPrototypePush as push, IteratorPrototype, ObjectCreate, ObjectDefineProperty, ObjectSetPrototypeOf, ReflectApply as apply, SymbolToStringTag, TypeError, WeakMap, WeakMapPrototypeGet as get, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { array, required, state, usv } from "ext:web/idl.js"; import { blobs } from "ext:blob/blob.js"; import { fileEntry } from "ext:blob/file.js"; import type { File } from "ext:blob/file.js"; type FormValue = string | File; interface FormEntry { name: string; value: FormValue; } interface FormState { list: FormEntry[]; } type IteratorValues = { 0: [string, FormValue]; 1: string; 2: FormValue }; const forms = new WeakMap(), arrayPrototype = Array.prototype; registerUncloneable(forms); function entry(rawName: unknown, value: unknown, rawFilename: unknown, count: number): FormEntry { const name = usv(rawName); const blob = get(blobs, value); if (count >= 3 && !blob) { throw new TypeError("Expected a Blob when filename is supplied"); } if (!blob) return { name, value: usv(value) }; const filename = rawFilename === undefined ? undefined : usv(rawFilename); return { name, value: fileEntry(value, filename) }; } // Iterator inheritance is installed below, without introducing a super call. interface FormDataIterator extends IterableIterator {} class FormDataIterator { #data: FormState; #index = 0; #kind: K; constructor(data: FormState, kind: K) { markUncloneable(this); this.#data = data; this.#kind = kind; } next(): IteratorResult { const data = this.#data; if (this.#index >= data.list.length) { return { value: undefined, done: true }; } const e = data.list[this.#index++]; return { value: (this.#kind === 0 ? [e.name, e.value] : this.#kind === 1 ? e.name : e.value) as IteratorValues[K], done: false, }; } } delete (FormDataIterator.prototype as { constructor?: Function }).constructor; ObjectSetPrototypeOf(FormDataIterator.prototype, IteratorPrototype); ObjectDefineProperty(FormDataIterator.prototype, "next", { enumerable: true }); ObjectDefineProperty(FormDataIterator.prototype, SymbolToStringTag, { value: "FormData Iterator", configurable: true, }); interface FormData { [Symbol.iterator](): FormDataIterator<0>; } class FormData { constructor(form: unknown = undefined, submitter: unknown = null) { if (form !== undefined || (submitter !== undefined && submitter !== null)) { throw new TypeError("not yet implemented: DOM-backed FormData"); } set(forms, this, { list: array() }); } append(name: unknown, value: unknown, filename: unknown = undefined): void { const data = state(forms, this); required(arguments.length, 2); push(data.list, entry(name, value, filename, arguments.length)); } set(name: unknown, value: unknown, filename: unknown = undefined): void { const data = state(forms, this); required(arguments.length, 2); const e = entry(name, value, filename, arguments.length); let found = false, write = 0; for (let i = 0; i < data.list.length; i++) { const current = data.list[i]; if (current.name !== e.name) data.list[write++] = current; else if (!found) { data.list[write++] = e; found = true; } } data.list.length = write; if (!found) push(data.list, e); } delete(rawName: unknown): void { const data = state(forms, this); required(arguments.length, 1); const name = usv(rawName); let write = 0; for (let i = 0; i < data.list.length; i++) { const current = data.list[i]; if (current.name !== name) data.list[write++] = current; } data.list.length = write; } get(rawName: unknown): FormValue | null { const data = state(forms, this); required(arguments.length, 1); const name = usv(rawName); for (let i = 0; i < data.list.length; i++) { if (data.list[i].name === name) return data.list[i].value; } return null; } getAll(rawName: unknown): FormValue[] { const data = state(forms, this); required(arguments.length, 1); const name = usv(rawName); const result = array(); for (let i = 0; i < data.list.length; i++) { if (data.list[i].name === name) push(result, data.list[i].value); } ObjectSetPrototypeOf(result, arrayPrototype); return result; } has(rawName: unknown): boolean { const data = state(forms, this); required(arguments.length, 1); const name = usv(rawName); for (let i = 0; i < data.list.length; i++) { if (data.list[i].name === name) return true; } return false; } forEach(callback: (value: FormValue, key: string, parent: FormData) => void, thisArg: unknown = undefined): void { const data = state(forms, this); required(arguments.length, 1); if (typeof callback !== "function") { throw new TypeError("Expected a callback"); } for (let i = 0; i < data.list.length; i++) { const e = data.list[i]; apply(callback, thisArg, [e.value, e.name, this]); } } entries(): FormDataIterator<0> { return new FormDataIterator(state(forms, this), 0); } keys(): FormDataIterator<1> { return new FormDataIterator(state(forms, this), 1); } values(): FormDataIterator<2> { return new FormDataIterator(state(forms, this), 2); } } function fromEntries(list: FormEntry[]): FormData { const result: FormData = ObjectCreate(FormData.prototype); set(forms, result, { list }); return result; } export { FormData, forms, fromEntries }; export type { FormEntry };