From 4a8f633179efbfb3ffcb1939628dac5262088116 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 3 Jun 2026 03:54:05 +0200 Subject: [PATCH] words/engine: table factory seeds block cells, not inline nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the column-insert crash + "can't edit any cell". Since P5m, `TableCell.children` holds WordsBlock[], but the slash-menu "Table" factory still seeded cells with bare `emptyText()` (inline `text` nodes). `normalizeBlock` has no case for `'text'` and returns undefined, so `normalizeTableCells` stored `[undefined]` — which then crashed `getActiveMarksForSelection`'s walk and left the cell with no real block to edit. - built-ins.ts: wrap each seeded cell in a paragraph block (matches the demo doc, the callout factory, and `createTableCell`). - normalize.ts: `normalizeTableCells` now drops children that fail to normalize and re-seeds an empty paragraph when none remain — self-heals any document that already got a malformed table from the old factory. Pairs with the defensive guard in selection-walkers (76b0d1c8). Engine tests 430/430 pass; changed files type-check clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../words/engine/blocks/built-ins.ts | 18 ++++++++++++++++-- .../words/engine/operations/normalize.ts | 16 ++++++++++------ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/uix/soma/components/words/engine/blocks/built-ins.ts b/src/uix/soma/components/words/engine/blocks/built-ins.ts index 90a2b0ffd..c9a91033a 100644 --- a/src/uix/soma/components/words/engine/blocks/built-ins.ts +++ b/src/uix/soma/components/words/engine/blocks/built-ins.ts @@ -361,9 +361,23 @@ const tableSpec: WordsBlockSpec = { group: 'structure', create: () => ({ type: 'table', + // Cells hold BLOCKS (P5m), not inlines: seed each with an empty + // paragraph. Passing bare `emptyText()` (inline nodes) here makes + // `normalizeBlock` return undefined for the unrecognized 'text' + // type, corrupting the cell (crashes the mark-walk, breaks editing). rows: [ - { cells: [{ children: emptyText() }, { children: emptyText() }] }, - { cells: [{ children: emptyText() }, { children: emptyText() }] } + { + cells: [ + { children: [{ type: 'paragraph', children: emptyText() }] }, + { children: [{ type: 'paragraph', children: emptyText() }] } + ] + }, + { + cells: [ + { children: [{ type: 'paragraph', children: emptyText() }] }, + { children: [{ type: 'paragraph', children: emptyText() }] } + ] + } ] }) } diff --git a/src/uix/soma/components/words/engine/operations/normalize.ts b/src/uix/soma/components/words/engine/operations/normalize.ts index 2fe1d684d..9a3b21aa4 100644 --- a/src/uix/soma/components/words/engine/operations/normalize.ts +++ b/src/uix/soma/components/words/engine/operations/normalize.ts @@ -207,12 +207,16 @@ function normalizeTableCells( } let mutated = false; const next = cells.map((cell) => { - // Cells hold blocks (like callout): normalize each child block; seed - // an empty paragraph when the cell has none. - const children = - cell.children.length > 0 - ? cell.children.map((child) => normalizeBlock(child, idGen)) - : seedCellChildren(idGen); + // Cells hold blocks (like callout): normalize each child block, then + // DROP any that failed to normalize. A stray inline slot — e.g. a cell + // seeded with bare `emptyText()` by the pre-fix table factory — makes + // `normalizeBlock` return undefined; keeping it would crash the + // mark-walk and break cell editing. Re-seed an empty paragraph when + // nothing valid remains, so a cell always holds at least one block. + const mapped = cell.children + .map((child) => normalizeBlock(child, idGen)) + .filter((b) => b != null); + const children = mapped.length > 0 ? mapped : seedCellChildren(idGen); const cellId = cell.id ?? idGen(); const childrenChanged = children.length !== cell.children.length || children.some((c, i) => c !== cell.children[i]);