/** * setSlideshowManifest — Studio persist helper for the slideshow JSON island. * * The island is a ` // blocks (global + case-insensitive) so we can strip every stale island in one pass. const ISLAND_RE = slideshowIslandRegex("gi"); export function buildSlideshowIslandHtml(manifest: SlideshowManifest): string { // Stamp the schema version (preserve an existing one) so future schema // changes can detect and migrate older islands. const versioned: SlideshowManifest = { version: manifest.version ?? SLIDESHOW_MANIFEST_VERSION, ...manifest, }; // Escape `<` and `>` so that a manifest field containing `` cannot // break out of the script tag. JSON.parse round-trips unchanged. const json = JSON.stringify(versioned, null, 2).replace(//g, "\\u003e"); return ``; } export interface PersistSlideshowArgs { manifest: SlideshowManifest; /** Live SDK Composition session — used only to read the current serialized HTML. */ sdkSession: Pick; /** Exact on-disk bytes for the undo-history `before` baseline. */ originalContent: string; targetPath: string; deps: CutoverDeps; /** Optional label override (default: "Edit slideshow"). */ label?: string; /** * When provided, threads a coalesceKey into recordEdit so rapid writes * (e.g. per-keystroke notes changes) collapse to a single undo entry. */ coalesceKey?: string; } export async function persistSlideshowManifest(args: PersistSlideshowArgs): Promise { const { manifest, sdkSession, originalContent, targetPath, deps, label, coalesceKey } = args; const islandHtml = buildSlideshowIslandHtml(manifest); // Write-time validation: confirm the island we just built round-trips to a // valid manifest before touching disk, so a malformed edit can't corrupt the // composition. parseSlideshowManifest throws on a structurally-invalid island. try { if (!parseSlideshowManifest(islandHtml)) { throw new Error("built island did not parse back to a manifest"); } } catch (err) { throw new Error(`refusing to persist invalid slideshow manifest: ${(err as Error).message}`); } const current = sdkSession.serialize(); // Strip ALL existing islands (handles the case where two stale islands // accumulated) then insert exactly one fresh island. const stripped = current.replace(ISLAND_RE, ""); let after: string; const bodyClose = stripped.lastIndexOf(""); if (bodyClose !== -1) { after = stripped.slice(0, bodyClose) + islandHtml + "\n" + stripped.slice(bodyClose); } else { after = stripped + "\n" + islandHtml; } // No-op gate: if the rewritten HTML is byte-identical to the current serialized // HTML, skip the write — avoids a spurious disk write and a no-op undo entry. if (after === current) return; await persistSdkSerialize(after, targetPath, originalContent, deps, { label: label ?? "Edit slideshow", ...(coalesceKey ? { coalesceKey } : {}), }); }