/**
* SSR / hydration helpers for forms.
*
* @module bquery/forms
*/
import type { Form, FormSnapshot } from './types';
import { escapeForScript } from '../ssr/escape';
const FORM_STATE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
const validateFormStateId = (id: string): string => {
const value = String(id);
if (!FORM_STATE_ID_PATTERN.test(value)) {
throw new Error(
'bQuery forms: form state id must contain only letters, numbers, underscores, or hyphens.'
);
}
return value;
};
/**
* `JSON.stringify` replacer that enforces the **guaranteed serialization
* boundary** for SSR form state. Values that cannot survive a JSON round-trip
* in a meaningful, hydration-safe form are deterministically dropped (the key
* is omitted) rather than serialized as `null`, `{}`, or `"[object File]"`:
*
* - **functions** — not transferable; intentionally dropped.
* - **`File` / `Blob` / `FileList`** — binary handles cannot cross the
* server→HTML→client boundary; dropped. Re-attach large blobs on the client
* after hydration.
* - **`bigint`** — would throw in `JSON.stringify`; dropped for safety.
* - **`symbol` / `undefined`** — already omitted by `JSON.stringify`; listed
* here for completeness.
*
* This makes the boundary an explicit, tested contract instead of an incidental
* side effect of `JSON.stringify`.
*
* @internal
*/
const isNonSerializable = (value: unknown): boolean => {
if (typeof value === 'function' || typeof value === 'bigint' || typeof value === 'symbol') {
return true;
}
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
if (typeof File !== 'undefined' && value instanceof File) return true;
if (typeof FileList !== 'undefined' && value instanceof FileList) return true;
return false;
};
const stripNonSerializable = (_key: string, value: unknown): unknown => {
if (isNonSerializable(value)) return undefined;
// An array can't carry a hole, so a dropped element would serialize as
// `null` and break the "dropped, never null" contract. Remove the offending
// elements from the array instead; nested arrays are handled recursively as
// the replacer revisits them.
if (Array.isArray(value) && value.some(isNonSerializable)) {
return value.filter((element) => !isNonSerializable(element));
}
return value;
};
/**
* Serialize a form snapshot to an inline ``;
};
/**
* Read a previously-serialized form snapshot from the DOM. Returns `undefined`
* if no matching `