/** * block-content — pure content-shape helpers for text blocks. * * The editor's blank-area flow (page-view) needs three answers about a * block, and all of them must be DOM-free + deterministic (unit-testable * without a browser, per AGENTS.md "prefer deterministic behavior"): * * 1. `isAutoCleanableType(type)` — which block types the empty-block * cleanup may touch (text-editable types only). * 2. `isContentEmpty(content)` — is the stored content empty? * 3. `isEmptyTextBlock(block)` — the full guard: text-editable type, * NOT the page title, and empty content. * * WHY THE STRICT CONTENT CHECK (never guess "empty"): * Deleting a block is irreversible. The stored `content` has exactly two * shapes in the wild (see `InlineEditor.toHtml`): * * - `string` — web edits store Tiptap HTML. Empty ⇔ no tags AND no * non-whitespace text (`""`, `"

"`, `"


"`). * - `{}` — the API/Prisma default for a freshly-created block (nothing * ever typed). Empty: an object with no keys provably carries no data, * so deleting/reusing it can lose nothing. * - `{ text?: string }` / `{ title?: string }` — the API/AI canonical * envelope (page-title block, blocks created via API/import/AI). * Empty ⇔ no displayable text. * * Any OTHER unrecognized object shape (tables, arrays, nested docs, * non-empty objects) is treated as NON-EMPTY. A false negative just * leaves a block around; a false positive deletes user data. * * WHY `isTitle` IS PROTECTED: * Every page is seeded with a title block (a top-level `paragraph` with * `properties.isTitle: true`, see page.service.ts `create`). It shares the * top-level list with content blocks, so the type check alone cannot * distinguish it — the flag must. Deleting it would orphan the page's * title; the header already edits the title via `PATCH /pages/:id`. */ /** * Block types edited through the inline text editor (Tiptap) that may be * auto-cleaned. These are exactly the types rendered via `InlineEditor`, * which is where the `onEmptyBlur` hook + the shared caret-mover * (`focusBlockEditable`) attach. * * Deliberately EXCLUDED: * - `toggle` — a toggle with no title text can still own CHILD blocks (a * container — deleting it cascade-deletes its children), and its child * rows are nested rows outside the top-level blank-area scope. * - `code` — edited by `CodeBlock`'s own `