/** * version-text — plain-text extraction from a version-snapshot block's * stored `content`, for the read-only compare/preview panel (T-150/T-151). * * Stored content is one of (see inline-editor `toHtml`, the canonical * renderer): a raw HTML string (legacy blocks), the API canonical * `{ text }` / `{ title }` shape, or a serialized Tiptap doc object. The * panel never renders HTML (a version snapshot is untrusted input) — it * extracts text only, which is all a block-level diff summary needs. */ export function blockText(content: unknown): string { if (content == null) return ""; if (typeof content === "string") return stripHtml(content); if (typeof content === "object") { const c = content as { text?: unknown; title?: unknown }; if (typeof c.text === "string") return c.text; if (typeof c.title === "string") return c.title; // Tiptap doc object: walk doc → content[] → text nodes (best effort — // the MVP stores `{ text }`/HTML, not full docs, so this is a fallback). const t = walkTiptapText(content); if (t) return t; } return ""; } /** `stripHtml` for a string payload: DOMParser in the browser, tag-stripping * regex fallback elsewhere (tests). */ export function stripHtml(html: string): string { if (typeof DOMParser !== "undefined") { return new DOMParser().parseFromString(html, "text/html").body.textContent ?? ""; } return html.replace(/<[^>]*>/g, ""); } function walkTiptapText(node: unknown, depth = 0): string { if (!node || typeof node !== "object" || depth > 8) return ""; const n = node as { type?: string; text?: unknown; content?: unknown }; if (typeof n.text === "string") return n.text; if (Array.isArray(n.content)) return n.content.map((c) => walkTiptapText(c, depth + 1)).join(""); return ""; }