words/engine: table factory seeds block cells, not inline nodes

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) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 76b0d1c8de
commit 4a8f633179

@ -361,9 +361,23 @@ const tableSpec: WordsBlockSpec = {
group: 'structure', group: 'structure',
create: () => ({ create: () => ({
type: 'table', 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: [ 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() }] }
]
}
] ]
}) })
} }

@ -207,12 +207,16 @@ function normalizeTableCells(
} }
let mutated = false; let mutated = false;
const next = cells.map((cell) => { const next = cells.map((cell) => {
// Cells hold blocks (like callout): normalize each child block; seed // Cells hold blocks (like callout): normalize each child block, then
// an empty paragraph when the cell has none. // DROP any that failed to normalize. A stray inline slot — e.g. a cell
const children = // seeded with bare `emptyText()` by the pre-fix table factory — makes
cell.children.length > 0 // `normalizeBlock` return undefined; keeping it would crash the
? cell.children.map((child) => normalizeBlock(child, idGen)) // mark-walk and break cell editing. Re-seed an empty paragraph when
: seedCellChildren(idGen); // 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 cellId = cell.id ?? idGen();
const childrenChanged = const childrenChanged =
children.length !== cell.children.length || children.some((c, i) => c !== cell.children[i]); children.length !== cell.children.length || children.some((c, i) => c !== cell.children[i]);

Loading…
Cancel
Save

Powered by TurnKey Linux.