Skip to content

feat(editor): add table support - #1368

Merged
bobbyquantum merged 3 commits into
mainfrom
feat/editor-tables
Aug 31, 2026
Merged

feat(editor): add table support#1368
bobbyquantum merged 3 commits into
mainfrom
feat/editor-tables

Conversation

@bobbyquantum

Copy link
Copy Markdown
Owner

Adds tables to the document editor.

Why not the upstream PR

There is an open table PR on ngx-editor (sibiraj-s/ngx-editor#610, open since June 2025, maintainer last responded a year ago). Roughly 70% of it is menu components — and Inkweld replaced ngx-editor's menu with its own Material toolbar, so those would be dead weight. Only the schema and editing plugins are useful, and both can be wired up on this side, so @bobbyquantum/ngx-editor stays untouched.

Editor

  • tableNodes() merged into the schema in extended-schema.ts. cellContent is 'paragraph+', not the library's suggested 'block+' — arbitrary blocks would permit nested tables and headings that no export format renders sensibly. An extra align cell attribute carries GFM column alignment.
  • columnResizing + tableEditing + a Tab / Shift-Tab keymap appended to the editor's plugin list.
  • Toolbar menu: insert, add/delete row and column, merge and split cells, header row/column toggles, delete table. Disabled outside a table.
  • Inserting a table at the end of a document also appends a paragraph. Without it, a trailing table is a dead end — there is no inline position after it to place the cursor.

Wire format and markdown

  • Table node names added to BLOCK_NODE_NAMES, so an empty cell serializes as <table_cell></table_cell> rather than collapsing to a self-closing tag and dropping a column on re-parse.
  • markdownToXml parses GFM tables: :--- / :---: / ---: alignment, escaped pipes, and ragged rows (padded or truncated to the header width, since ProseMirror requires rectangular rows).
  • xmlToMarkdown emits them again. The round trip is byte-stable.
  • The update_document_content MCP tool documents both formats.

Publish

Tables render in all four output formats. Merged cells survive in PDF; Markdown, HTML, and EPUB flatten a merge into the cell plus empty columns so the grid keeps its shape. Column alignment is preserved everywhere.

Tables are not yet part of the user-configurable publish-styles system — they get fixed built-in styling for now. That is a deliberate scope cut; wiring them into DocNodeKey touches the style model, presets, and the style editor UI.

A note on the dependency

prosemirror-tables is pinned in resolutions alongside the other ProseMirror packages. It depends on prosemirror-model, -state, and -view, and a second copy of prosemirror-model breaks class-identity checks in y-prosemirror and EditorView — typing silently stops working, which is the failure mode from #1068. Verified there is still exactly one copy of each after install.

Testing

  • 17 new unit tests in @inkweld/prosemirror covering both markdown directions, the round trip, and survival through the Yjs sync path
  • 16 new toolbar unit tests
  • 1 new publish test asserting GFM output with header and alignment
  • 3 new e2e specs: insert, edit (typing, Tab, row/column commands), and persistence across a reload
  • Full suites green: 217 package / 7491 frontend / 1420 backend unit tests, 76 local e2e, docs site builds

Docs

User guide gets a Tables section under Formatting, table shortcuts under Keyboard Shortcuts, a toolbar entry, and an export-fidelity note. Developer docs get an Editor Schema section in architecture.md, and AGENTS.md gains a checklist of the files that must stay in step when touching tables.

Not in this PR

  • Tables in the user-configurable publish styles
  • Importing tables from .docx or pasted HTML

One thing worth knowing

Reloading a document that starts with a table logs TextSelection endpoint not pointing into a node with inline content from y-prosemirror, which restores the selection at position 0 before the first node. This is pre-existing and not table-specific — a document starting with a horizontal rule does the same thing on main. ProseMirror recovers and content is unaffected, so I left it alone rather than widening this PR.

Adds tables to the document editor, built directly on `prosemirror-tables`
rather than on the upstream ngx-editor table PR (sibiraj-s/ngx-editor#610).
That PR is roughly 70% menu components, and Inkweld already replaced
ngx-editor's menu with its own Material toolbar — only the schema and
editing plugins are useful here, so the fork stays untouched.

Editor:
- `tableNodes()` merged into the schema in `extended-schema.ts`, with
  `cellContent: 'paragraph+'` so cells cannot hold nested tables or
  headings that no export format renders sensibly, plus an `align` cell
  attribute for GFM column alignment
- `columnResizing` + `tableEditing` + a Tab/Shift-Tab keymap appended to
  the editor's plugin list
- Material toolbar menu covering insert, row/column add and delete, merge
  and split, header toggles, and delete table
- Inserting a table at the end of a document also appends a paragraph, so
  a trailing table is not a dead end for the cursor

Wire format and markdown:
- table node names added to `BLOCK_NODE_NAMES`, so an empty cell
  serializes as `<table_cell></table_cell>` instead of collapsing to a
  self-closing tag and desynchronising the row
- `markdownToXml` parses GFM tables including `:---` / `:---:` / `---:`
  alignment, escaped pipes, and ragged rows (padded to the header width)
- `xmlToMarkdown` emits them again; the round trip is byte-stable
- `update_document_content` MCP tool documents both

Publish: tables render in Markdown, HTML, EPUB, and Typst/PDF output.
Merged cells survive in PDF; the other three flatten a merge into the
cell plus empty columns so the grid keeps its shape. Tables are not yet
part of the user-configurable publish-styles system.

`prosemirror-tables` is pinned in `resolutions` alongside the other
ProseMirror packages — a second copy of `prosemirror-model` would break
class-identity checks in y-prosemirror and silently stop typing (#1068).
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 15ca42c4-905a-4943-8d61-1e9df7624a6a


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bobbyquantum
bobbyquantum deployed to sonarcloud-analysis August 29, 2026 21:44 — with GitHub Actions Active
Two problems, both in tests rather than in the table implementation.

**Toolbar spec crashed CI.** The mock selection defined only `$from` and
`$to`. The real `isInTable()` reads `selection.$head`, so it threw inside
the debounced selection-state update — failing the table tests and leaking
unhandled errors into the overflow tests in the same file. With
`maxForks: 1` in CI the worker died and the run ended in `write EPIPE`
rather than a readable failure list.

`vi.mock('prosemirror-tables', ...)` from `setup-vitest.ts` turned out not
to apply reliably — under coverage instrumentation the real module loads
regardless, which is why the suite passed locally without `--coverage` and
failed in CI. Rather than fight that, the mock is gone and the specs now
run against the real library: every table command bails out through
`isInTable()` (or an equivalent guard) when the selection is not in a
table, so the real implementations are safe against a mock view. Mock
selections gain `$head`/`$anchor` and mock schemas a `cached` object,
because that is what the real code reads. The command assertions check
what the toolbar actually owns — the command runs without throwing and
focus returns to the editor — while the e2e specs cover the mutations.

**Publish table output was untested**, which is most of the coverage gap
Sonar flagged (failing tests also contribute no coverage). Added tests for
the HTML, EPUB and Typst/PDF table paths, plus the schema's `align` cell
attribute. Line coverage on the touched files is now 84–100%.

Two things surfaced while writing them:

- EPUB requires `type: 'text'` on text nodes while HTML only checks for a
  `text` property, so the original test data silently rendered empty cells.
  Real ProseMirror JSON always sets it; the data was wrong, not the code.
  All the table fixtures now use the realistic shape.
- The schema's `align` callbacks are folded into the generated
  parseDOM/toDOM rather than exposed on the attr spec, so they are covered
  through a real DOM round trip instead — which also pins down that an
  align value cannot smuggle extra CSS declarations into the output.
@bobbyquantum
bobbyquantum deployed to sonarcloud-analysis August 29, 2026 23:11 — with GitHub Actions Active
Four code smells SonarCloud raised against the new table code. None change
behaviour.

- `splitTableRow` used `cells[cells.length - 1]`; `.at(-1)` says the same
  thing and drops the now-redundant length guards.
- `parseDelimiterRow`'s `!line || !line.includes('|')` collapses to
  `!line?.includes('|')`.
- The table plugins went into a second consecutive `plugins.push()`; folded
  into the preceding one. Array order — and so plugin precedence — is
  unchanged.
- `EpubGeneratorService.getTagAndClass` hit cognitive complexity 22 once the
  table branch was added. Its node-tag map is now a module-level constant
  rather than an object literal rebuilt on every node, and name resolution
  and class-list building are separate helpers.
@bobbyquantum
bobbyquantum deployed to sonarcloud-analysis August 29, 2026 23:37 — with GitHub Actions Active
@sonarqubecloud

Copy link
Copy Markdown

@bobbyquantum bobbyquantum added the deploy:preview:frontend Deploy a frontend preview for this PR to Cloudflare Pages label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Frontend preview deployed for #1368

This is a frontend-only preview (no backend). It runs in local/offline mode; point it at an existing server at runtime through the setup flow if needed. On PR close or label removal, the cleanup workflow attempts to delete this branch's Pages deployments; note that Cloudflare keeps the latest deployment for a branch, so the preview URL may remain reachable after cleanup.

@bobbyquantum
bobbyquantum merged commit 16601fc into main Aug 31, 2026
32 checks passed
@bobbyquantum
bobbyquantum deleted the feat/editor-tables branch August 31, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy:preview:frontend Deploy a frontend preview for this PR to Cloudflare Pages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant