Fifth atomic checkpoint of F2.3. Moves the table-specific DOM render
helpers from engine/render.ts to extensions/table/render.ts.
Moved:
- renderTable(block, path, helpers) — replaces the table branch in
renderWordsBlock + internal renderWordsTableRow + renderWordsTableCell
- renderTablePlainText(block, helpers) — replaces the table branch in
renderBlockPlainText
The extension defines structural render-element types
(TableRenderElement, TableRenderText, TableRenderNode) assignment-
compatible with the engine's WordsRenderElement/Node. The engine
passes its constants (WORDS_PATH_ATTR, WORDS_NODE_ATTR) + helpers
(encodePath, renderInlines, getInlineText) via a TableRenderHelpers
object, keeping the extension independent of engine/dom.ts.
The engine's renderWordsBlock dispatcher branch for `block.type === 'table'`
now delegates to `renderTable(block, path, { ... })` in a single call.
~50 LoC of helper functions removed from the engine.
Extension barrel updated to expose:
- renderTable / renderTablePlainText
- TableRenderElement / TableRenderNode / TableRenderText / TableRenderHelpers / RenderPath types
Verification:
- 143/143 tests pass in src/uix/soma/components/words
- 0 new TypeScript errors
F2.3 progress: a + b + c + d + e done. Remaining: f (path), g
(normalize), h (operations — largest at ~400-500 LoC), i (registry
wire-up), j (verify). Operations remains the heaviest piece — likely
its own dedicated session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
parent
5797e69b88
commit
d839273536
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Table extension — DOM render hooks.
|
||||
*
|
||||
* Owns the conversion of WordsTableBlock / WordsTableRow / WordsTableCell
|
||||
* to the engine's intermediate render-element shape. The engine's render
|
||||
* dispatcher calls into here for `block.type === 'table'`.
|
||||
*
|
||||
* Render-internal helpers (encodeWordsPath, WORDS_NODE_ATTR, etc.) come
|
||||
* from the engine via constructor parameters to keep this module
|
||||
* decoupled from `engine/dom.ts` constants.
|
||||
*/
|
||||
|
||||
import type { WordsInline } from '../../engine/document';
|
||||
import type { WordsTableBlock, WordsTableCell, WordsTableRow } from './types';
|
||||
|
||||
// Render-element types kept structural to avoid importing the engine
|
||||
// shape. The engine's WordsRenderElement is assignment-compatible.
|
||||
export interface TableRenderElement {
|
||||
readonly kind: 'element';
|
||||
readonly tag:
|
||||
| 'table'
|
||||
| 'tbody'
|
||||
| 'tr'
|
||||
| 'td'
|
||||
| 'th'
|
||||
| string;
|
||||
readonly attrs: Readonly<Record<string, string | undefined>>;
|
||||
readonly children: ReadonlyArray<TableRenderNode>;
|
||||
}
|
||||
|
||||
export interface TableRenderText {
|
||||
readonly kind: 'text';
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export type TableRenderNode = TableRenderElement | TableRenderText;
|
||||
|
||||
export type RenderPath = readonly number[];
|
||||
|
||||
export interface TableRenderHelpers {
|
||||
/** Path attr key (engine's `WORDS_PATH_ATTR`). */
|
||||
readonly pathAttr: string;
|
||||
/** Node attr key (engine's `WORDS_NODE_ATTR`). */
|
||||
readonly nodeAttr: string;
|
||||
/** Engine path encoder (engine's `encodeWordsPath`). */
|
||||
readonly encodePath: (path: RenderPath) => string;
|
||||
/** Engine inline renderer for the cell contents. */
|
||||
readonly renderInlines: (
|
||||
inlines: readonly WordsInline[],
|
||||
path: RenderPath
|
||||
) => ReadonlyArray<TableRenderNode>;
|
||||
/** Engine plain-text helper for cell text serialization. */
|
||||
readonly getInlineText: (inline: WordsInline) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a table block to the engine's intermediate element tree.
|
||||
*
|
||||
* Mirrors what `renderWordsBlock(block, path)` produced for tables
|
||||
* before F2.3e — same DOM shape (`<table><tbody><tr><td|th>`), same
|
||||
* attributes, same path encoding. Pure function: takes the block + a
|
||||
* path + helpers, returns a render-element.
|
||||
*/
|
||||
export function renderTable(
|
||||
block: WordsTableBlock,
|
||||
path: RenderPath,
|
||||
helpers: TableRenderHelpers
|
||||
): TableRenderElement {
|
||||
return {
|
||||
kind: 'element',
|
||||
tag: 'table',
|
||||
attrs: {
|
||||
[helpers.nodeAttr]: 'table',
|
||||
[helpers.pathAttr]: helpers.encodePath(path),
|
||||
'data-words-block': 'table',
|
||||
...(block.striped ? { 'data-words-table-striped': '' } : {}),
|
||||
...(block.compact ? { 'data-words-table-compact': '' } : {})
|
||||
},
|
||||
children: [
|
||||
{
|
||||
kind: 'element',
|
||||
tag: 'tbody',
|
||||
attrs: {},
|
||||
children: block.children.map((row, rowIndex) =>
|
||||
renderTableRow(row, [...path, rowIndex], helpers)
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function renderTableRow(
|
||||
row: WordsTableRow,
|
||||
path: RenderPath,
|
||||
helpers: TableRenderHelpers
|
||||
): TableRenderElement {
|
||||
return {
|
||||
kind: 'element',
|
||||
tag: 'tr',
|
||||
attrs: {
|
||||
[helpers.nodeAttr]: 'table-row',
|
||||
[helpers.pathAttr]: helpers.encodePath(path)
|
||||
},
|
||||
children: row.children.map((cell, cellIndex) =>
|
||||
renderTableCell(cell, [...path, cellIndex], helpers)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function renderTableCell(
|
||||
cell: WordsTableCell,
|
||||
path: RenderPath,
|
||||
helpers: TableRenderHelpers
|
||||
): TableRenderElement {
|
||||
return {
|
||||
kind: 'element',
|
||||
tag: cell.header ? 'th' : 'td',
|
||||
attrs: {
|
||||
[helpers.nodeAttr]: 'table-cell',
|
||||
[helpers.pathAttr]: helpers.encodePath(path),
|
||||
...(cell.header ? { 'data-words-cell-header': '' } : {}),
|
||||
...(cell.textAlign && cell.textAlign !== 'left'
|
||||
? {
|
||||
'data-words-cell-align': cell.textAlign,
|
||||
style: `text-align: ${cell.textAlign}`
|
||||
}
|
||||
: {}),
|
||||
...(cell.verticalAlign && cell.verticalAlign !== 'top'
|
||||
? { 'data-words-cell-vertical': cell.verticalAlign }
|
||||
: {}),
|
||||
...(cell.tone && cell.tone !== 'default' ? { 'data-words-cell-tone': cell.tone } : {})
|
||||
},
|
||||
children: helpers.renderInlines(cell.children, path)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a table block to plain text — cells tab-separated, rows
|
||||
* newline-separated. Used by the engine's plain-text serializer for
|
||||
* features like clipboard copy of a single block. Pure function.
|
||||
*/
|
||||
export function renderTablePlainText(
|
||||
block: WordsTableBlock,
|
||||
helpers: Pick<TableRenderHelpers, 'getInlineText'>
|
||||
): string {
|
||||
return block.children
|
||||
.map((row) =>
|
||||
row.children
|
||||
.map((cell) => cell.children.map(helpers.getInlineText).join(''))
|
||||
.join('\t')
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
Loading…
Reference in new issue