diff --git a/continue.md b/continue.md index 535c001bd..cc4128f34 100644 --- a/continue.md +++ b/continue.md @@ -212,15 +212,16 @@ selectors y type unions coincidan. - F2.2 skeleton: `extension-types.ts` + `extension-registry.ts` con 14 tests verdes - F2.3a tipos de table movidos a `extensions/table/types.ts` con re-export en `engine/document.ts` -**F2.3 hecho** (sesión de hoy, 5 checkpoint commits + 143/143 tests verdes después de cada uno): +**F2.3 hecho** (sesiones 2026-05-27, 6 checkpoint commits + tests verdes después de cada uno): - **F2.3a** ✅ — table types → `extensions/table/types.ts` (commit `088d31ec`) - **F2.3b** ✅ — table factories + predicates + value-set constants → `extensions/table/factories.ts` (commit `a0afa44d`) - **F2.3d** ✅ — table markdown serializer (serialize + parse + buildTableFromMarkdownRows) → `extensions/table/serialize-markdown.ts` (commit `d6f0af48`) - **F2.3c** ✅ — table HTML serializer (serializeTableHtml + parseTableHtml + cell helpers) → `extensions/table/serialize-html.ts` (commit `5797e69b`) - **F2.3e** ✅ — table render (renderTable + renderTablePlainText) → `extensions/table/render.ts` (commit `d8392735`) +- **F2.3i (stub)** ✅ — `tableExtension: WordsExtension` con `nodeTypes` + `factories` + `commandNames` (12) publicado en `extensions/table/table-extension.ts`. Engine NO consume registry todavía — la stub publica el shape estable que F2.3f-h irán rellenando con hooks. 6 smoke tests + 102 tests en el scope extensions+engine en verde. -**F2.3 pendiente — los 4 sub-pasos restantes son el refactor difícil**: +**F2.3 pendiente — los 3 sub-pasos restantes son el refactor difícil** (la stub F2.3i existe ya como receptor de hooks): - **F2.3f** — path.ts navigation. Las refs de table están en BRANCHES dentro de funciones grandes (`resolvePath`, `updateNode`, `inferContainerKind`), NO en helpers aislados. Extracción requiere: - Diseñar callback adapters para recursión (engine → extension → engine) @@ -234,21 +235,20 @@ selectors y type unions coincidan. - Mover los ~20 reducers a `extensions/table/operations.ts` - El dispatcher central (`applyWordsCommand`) consulta registry.getCommand(opType) y fallback al switch existente -- **F2.3i** — registry wire-up. Crear `tableExtension: WordsExtension` que cabledea TODOS los hooks (commands, render, serialize, etc.) al registry. El engine constructor registra `tableExtension` por defecto para backward compatibility. Después el dispatcher reescrito consulta registry. +- **F2.3i (full)** — wire-up al engine constructor. Crear el patrón de instanciación del engine que (a) construye una `WordsExtensionRegistry`, (b) registra `tableExtension` por defecto, (c) hace que el dispatcher (`applyWordsCommand`), el render, el path, el normalize y los serializers consulten primero la registry y fallback al switch hardcodeado. Hoy `tableExtension` está como stub publicada — el engine la ignora. - **F2.3j** — verify final. -**Recomendación de orden para próxima sesión** (lo que dije y se ha confirmado al hacer F2.3a-e): +**Recomendación de orden para próxima sesión** (confirmado tras F2.3a-e + F2.3i-stub): -1. Antes de F2.3f/g/h, hacer **F2.3i con stub vacía**: construir `tableExtension: WordsExtension` que registra sólo nodeTypes ['table','table-row','table-cell'], commandNames y events. El engine al boot lo registra. Después en h-i se mueve la lógica gradualmente hacia los hooks. -2. **F2.3h primero** (operations) usando el stub. Cada reducer extraído pasa de `case 'insertTable':` en switch a entrada en `extension.commands`. El switch del engine consulta registry.getCommand() antes del fallback. -3. **F2.3f y F2.3g** al final, donde el engine ya consulta registry para todo el resto. +1. **F2.3h primero** (operations) — la stub `tableExtension` ya es receptor válido. Mover los ~20 reducers (`insertTable*`, `deleteTable*`, `toggle-table-*`, `setTableCell*`, `moveTableCell`) a `extensions/table/operations.ts`, llenar `tableExtension.commands` con sus key=opType. Cabledar el dispatcher central (`applyWordsCommand`) para consultar `registry.getCommand(opType)` antes del switch. Decisión clave: las utilities compartidas (`tableCellOptions`, `tableOptions`, `replaceAt`, `currentTableCellPath`) van a `extensions/table/utils.ts` o se re-exportan desde engine; la opción a re-exportar las dos primeras (engine NO depende de ellas fuera del table-branch) y mover el resto. +2. **F2.3f y F2.3g** al final, donde el engine ya consulta registry para todo el resto. Diseñar visitor pattern en `path.ts` + `normalize.ts` para que el dispatcher pase el callback "is this a node my extension owns?" al walker. **Estado del extension system** después de hoy: - `WordsExtension` interface ✅ (F2.2) - `WordsExtensionRegistry` con 14 tests verdes ✅ (F2.2) -- `extensions/table/` con types + factories + 2 serializers + render ✅ (F2.3a-e) -- Engine NO consulta registry aún (importa funciones directamente de extension). El cambio a "engine consulta registry para tipo X" llegará en F2.3i. +- `extensions/table/` con types + factories + 2 serializers + render + **`tableExtension` stub** ✅ (F2.3a-e + F2.3i-stub) +- Engine NO consulta registry aún (importa funciones directamente de extension; `tableExtension` publicada pero engine no la ve). El cambio a "engine consulta registry para tipo X" llegará en F2.3h. Después F2.4 (code-block, plan similar), F2.5 (docs `EXTENSIONS.md`), F2.6 (verify final). diff --git a/src/uix/soma/components/words/extensions/index.ts b/src/uix/soma/components/words/extensions/index.ts index 311573452..a792d5c3c 100644 --- a/src/uix/soma/components/words/extensions/index.ts +++ b/src/uix/soma/components/words/extensions/index.ts @@ -35,3 +35,9 @@ export { createWordsExtensionRegistry, WordsExtensionRegistryError } from './extension-registry'; + +// First built-in extension. The engine is NOT yet consuming the +// registry — `tableExtension` is published in stub form (nodeTypes + +// factories + commandNames only) so the public shape is stable while +// F2.3f-h port the hooks in. +export { tableExtension } from './table'; diff --git a/src/uix/soma/components/words/extensions/table/index.ts b/src/uix/soma/components/words/extensions/table/index.ts index 65d81c352..41a30aa18 100644 --- a/src/uix/soma/components/words/extensions/table/index.ts +++ b/src/uix/soma/components/words/extensions/table/index.ts @@ -8,14 +8,18 @@ * Pieces moved so far: * - Types (F2.3a) * - Factories + value-set predicates (F2.3b) + * - HTML serializer (F2.3c) + * - Markdown serializer (F2.3d) + * - Render hooks (F2.3e) + * - `tableExtension` stub (F2.3i — name + nodeTypes + factories + + * commandNames; render / normalize / serialize / path / commands + * hooks still hardcoded in the engine, to be moved in F2.3f-h). * * Pending: - * - Serializers (HTML + Markdown) (F2.3c, F2.3d) - * - Render (F2.3e) - * - Path / navigation (F2.3f) - * - Normalize (F2.3g) - * - Operations (F2.3h) - * - Registry wire-up + WordsExtension export (F2.3i) + * - Path / navigation hook (F2.3f) + * - Normalize hook (F2.3g) + * - Operations / commands (F2.3h) + * - Engine boot wire-up + handoff (F2.3 final) */ export type { @@ -61,3 +65,5 @@ export type { TableRenderNode, TableRenderText } from './render'; + +export { tableExtension } from './table-extension'; diff --git a/src/uix/soma/components/words/extensions/table/table-extension.test.ts b/src/uix/soma/components/words/extensions/table/table-extension.test.ts new file mode 100644 index 000000000..b5d26c47a --- /dev/null +++ b/src/uix/soma/components/words/extensions/table/table-extension.test.ts @@ -0,0 +1,85 @@ +/** + * Smoke test for the F2.3i `tableExtension` stub. + * + * Validates the public shape published by the stub — it does NOT + * exercise the engine integration (hooks are absent on purpose at this + * stage; F2.3f-h add them). + */ + +import { describe, expect, it } from 'vitest'; +import { createWordsExtensionRegistry } from '../extension-registry'; +import { tableExtension } from './table-extension'; + +describe('tableExtension (F2.3i stub)', () => { + it('declares the three table node types', () => { + expect(tableExtension.name).toBe('table'); + expect(tableExtension.nodeTypes).toEqual(['table', 'table-row', 'table-cell']); + }); + + it('exposes factories that produce valid nodes for each type', () => { + const factories = tableExtension.factories; + expect(factories).toBeDefined(); + const table = factories!.table(); + expect(table.type).toBe('table'); + const row = factories!['table-row'](); + expect(row.type).toBe('table-row'); + const cell = factories!['table-cell'](); + expect(cell.type).toBe('table-cell'); + }); + + it('registers cleanly and is found by every nodeType', () => { + const reg = createWordsExtensionRegistry(); + reg.register(tableExtension); + expect(reg.findByNodeType('table')).toBe(tableExtension); + expect(reg.findByNodeType('table-row')).toBe(tableExtension); + expect(reg.findByNodeType('table-cell')).toBe(tableExtension); + expect(reg.allNodeTypes()).toEqual(['table', 'table-row', 'table-cell']); + }); + + it('exposes all 12 table command names through the registry', () => { + const reg = createWordsExtensionRegistry(); + reg.register(tableExtension); + const expected = [ + 'insertTable', + 'insertTableRow', + 'insertTableColumn', + 'deleteTableRow', + 'deleteTableColumn', + 'moveTableCell', + 'toggleTableHeaderRow', + 'toggleTableHeaderColumn', + 'toggleTableStriped', + 'toggleTableCompact', + 'setTableCellTone', + 'setTableCellVerticalAlign' + ]; + expect([...reg.allCommandNames()].sort()).toEqual(expected.slice().sort()); + for (const name of expected) { + expect(reg.findByCommandName(name)).toBe(tableExtension); + } + }); + + it('intentionally exposes no hooks yet (stub)', () => { + // F2.3i ships the static facets only. F2.3f-h add hooks. + expect(tableExtension.render).toBeUndefined(); + expect(tableExtension.normalize).toBeUndefined(); + expect(tableExtension.serialize).toBeUndefined(); + expect(tableExtension.path).toBeUndefined(); + expect(tableExtension.keyboard).toBeUndefined(); + expect(tableExtension.commands).toBeUndefined(); + expect(tableExtension.events).toBeUndefined(); + expect(tableExtension.dispose).toBeUndefined(); + }); + + it('cell factory accepts no args and produces a 1×1 default table', () => { + const factories = tableExtension.factories!; + const table = factories.table() as { + type: string; + children: ReadonlyArray<{ type: string; children: ReadonlyArray<{ type: string }> }>; + }; + expect(table.children).toHaveLength(1); + expect(table.children[0].type).toBe('table-row'); + expect(table.children[0].children).toHaveLength(1); + expect(table.children[0].children[0].type).toBe('table-cell'); + }); +}); diff --git a/src/uix/soma/components/words/extensions/table/table-extension.ts b/src/uix/soma/components/words/extensions/table/table-extension.ts new file mode 100644 index 000000000..33c28b309 --- /dev/null +++ b/src/uix/soma/components/words/extensions/table/table-extension.ts @@ -0,0 +1,80 @@ +/** + * Table extension — entrypoint that materialises the `WordsExtension` + * contract for the table node tree (`table` → `table-row` → `table-cell`). + * + * **F2.3i stub stage**: this file currently declares only the three + * static facets of the extension that we can publish without touching + * the engine's hardcoded branches: + * + * 1. `nodeTypes` — the three node `type` strings owned by the extension. + * 2. `factories` — typed constructors so callers can build empty + * `table` / `table-row` / `table-cell` nodes without importing + * the engine internals. + * 3. `commandNames` — the public command-name registry. Every + * table-related command in `engine/commands.ts` is mirrored here + * so the registry knows which public command belongs to this + * extension. The internal operation type matches the public name + * 1:1 today (no renames pending). + * + * The dynamic hooks (`render`, `normalize`, `serialize`, `path`, + * `keyboard`, `commands`) are intentionally absent. F2.3f-h port each + * one of those incrementally, at which point this object grows new + * fields; the public shape (this file) stays stable. + * + * The engine does NOT yet consult the registry at runtime — render / + * normalize / serialize / path / commands still hardcode the `table` + * branch. The extension is published so consumers (tests, future + * tooling, the eventual engine boot wire-up) can already see the shape + * the engine will respect once F2.3i full lands. + */ + +import type { WordsExtension, WordsExtensionNode } from '../extension-types'; +import { + createTable, + createTableCell, + createTableRow +} from './factories'; + +// Erase the typed factories to the registry's uniform shape. The factory +// functions remain individually typed for callers that import them +// directly; this adapter is only the contract-boundary view. +type ErasedFactory = (...args: ReadonlyArray) => WordsExtensionNode; + +const tableFactory = ((...args: ReadonlyArray) => + createTable(...(args as Parameters))) as ErasedFactory; + +const tableRowFactory = ((...args: ReadonlyArray) => + createTableRow(...(args as Parameters))) as ErasedFactory; + +const tableCellFactory = ((...args: ReadonlyArray) => + createTableCell(...(args as Parameters))) as ErasedFactory; + +export const tableExtension: WordsExtension = { + name: 'table', + version: '1.0.0', + nodeTypes: ['table', 'table-row', 'table-cell'], + factories: { + table: tableFactory, + 'table-row': tableRowFactory, + 'table-cell': tableCellFactory + }, + commandNames: { + // Public command name → internal operation type. They match 1:1 + // today; the indirection lets us rename one side without breaking + // the other when needed. + insertTable: 'insertTable', + insertTableRow: 'insertTableRow', + insertTableColumn: 'insertTableColumn', + deleteTableRow: 'deleteTableRow', + deleteTableColumn: 'deleteTableColumn', + moveTableCell: 'moveTableCell', + toggleTableHeaderRow: 'toggleTableHeaderRow', + toggleTableHeaderColumn: 'toggleTableHeaderColumn', + toggleTableStriped: 'toggleTableStriped', + toggleTableCompact: 'toggleTableCompact', + setTableCellTone: 'setTableCellTone', + setTableCellVerticalAlign: 'setTableCellVerticalAlign' + } + // render / normalize / serialize / path / keyboard / commands / + // events / dispose intentionally absent — see file header. +} satisfies WordsExtension;