feat(rte): opt-in 'table' token with GFM-constrained round-trip #102

Merged
dominik.polakovics merged 1 commit from afk/94 into main 2026-07-18 03:35:07 +02:00

Adds table as a ninth rte formatting token — opt-in only. The default allow-set stays the current 8 tokens; table must be named explicitly in an element's allow: list. text.yaml opts in with an explicit 9-token list.

Closes #94

Engine (Go)

  • internal/element: the valid token vocabulary (rteAllowTokens, 9 tokens) is decoupled from the default set (defaultRTEAllow, unchanged 8). Unknown-token errors now list the full vocabulary. The agentsdoc drift test iterates the full vocabulary, so AGENTS.md must name table.
  • internal/server/body.go: tables move from never-allowed to allow-gated ("tables are not allowed in this field" when the token is absent). When allowed, the walk descends into cells so their inline content is still policed — the no-raw-HTML gate is untouched (raw HTML in a cell is still rejected, as are disallowed marks). Markdown-policy bodies accept tables unconditionally, as before.

Editor (web)

  • allow.ts mirror: table joins AllowToken; new ALL_RTE_TOKENS (9) feeds validation; DEFAULT_RTE_ALLOW stays 8.
  • New rte/table.ts: tiptap table extensions (pinned 2.27.2), GFM-constrained — inline-only cells (inline*), merge/split and header-toggle commands inert, first row is structurally the header, no column resizing (no colwidth ever persists). Loaded only when the field's allow-set grants table; with the default set the schema has no table nodes at all.
  • markdown.ts: markdown-it's core table rule is enabled on opt-in; parser maps table tokens onto the tiptap nodes with GFM alignment captured per cell (textAlign). The serializer emits canonical single-space-padded pipe tables (delimiter row derived from the header row's alignment; | escaped as \|; hard breaks in cells degrade to a space; smuggled colspans/ragged rows are repaired to valid GFM — never HTML). Round-trip is byte-equivalent for canonical tables; a non-canonical hand-authored table normalizes once, then is a fixed point.
  • No creation UI ships in this slice — no toolbar button, no insert command, no paste-to-table mapping. Agent-authored pipe tables render as real tables and survive human edits of surrounding text.

Theme

  • New render hook layouts/_default/_markup/render-table.html wraps every rendered markdown table in <div class="ce-table" tabindex="0"> (horizontal scroll, keyboard-focusable), honoring per-column GFM alignment via inline text-align — the documented exception to the classes-only rule. Styled in theme.css, documented in the theme README. Verified with a live Hugo build.

Contract surfaces (per CLAUDE.md, same change)

Go allow-set, TS mirror, scaffold/site/AGENTS.md (token named; "omitting allow" rewording to "default set — every token above except table"), drift test.

Verification

  • go vet ./..., go test ./... (all packages, fresh run), go build — green (CGO_ENABLED=0; this sandbox has no gcc).
  • web: 816 tests / 40 files green, vite build succeeds, tsc --noEmit clean (CI parity: npm ci/build/test).
  • Hugo smoke test of the render hook shows the wrapper div, <thead>, and alignment styles in emitted HTML.

🤖 Generated with Claude Code

Adds `table` as a ninth rte formatting token — **opt-in only**. The default allow-set stays the current 8 tokens; `table` must be named explicitly in an element's `allow:` list. `text.yaml` opts in with an explicit 9-token list. Closes #94 ## Engine (Go) - `internal/element`: the valid token vocabulary (`rteAllowTokens`, 9 tokens) is decoupled from the default set (`defaultRTEAllow`, unchanged 8). Unknown-token errors now list the full vocabulary. The agentsdoc drift test iterates the full vocabulary, so AGENTS.md must name `table`. - `internal/server/body.go`: tables move from never-allowed to allow-gated ("tables are not allowed in this field" when the token is absent). When allowed, the walk descends into cells so their inline content is still policed — the no-raw-HTML gate is untouched (raw HTML in a cell is still rejected, as are disallowed marks). Markdown-policy bodies accept tables unconditionally, as before. ## Editor (web) - `allow.ts` mirror: `table` joins `AllowToken`; new `ALL_RTE_TOKENS` (9) feeds validation; `DEFAULT_RTE_ALLOW` stays 8. - New `rte/table.ts`: tiptap table extensions (pinned 2.27.2), GFM-constrained — **inline-only cells** (`inline*`), **merge/split and header-toggle commands inert**, **first row is structurally the header**, **no column resizing** (no colwidth ever persists). Loaded only when the field's allow-set grants `table`; with the default set the schema has no table nodes at all. - `markdown.ts`: markdown-it's core `table` rule is enabled on opt-in; parser maps table tokens onto the tiptap nodes with GFM alignment captured per cell (`textAlign`). The serializer emits canonical single-space-padded pipe tables (delimiter row derived from the header row's alignment; `|` escaped as `\|`; hard breaks in cells degrade to a space; smuggled colspans/ragged rows are repaired to valid GFM — never HTML). Round-trip is byte-equivalent for canonical tables; a non-canonical hand-authored table normalizes once, then is a fixed point. - **No creation UI ships in this slice** — no toolbar button, no insert command, no paste-to-table mapping. Agent-authored pipe tables render as real tables and survive human edits of surrounding text. ## Theme - New render hook `layouts/_default/_markup/render-table.html` wraps every rendered markdown table in `<div class="ce-table" tabindex="0">` (horizontal scroll, keyboard-focusable), honoring per-column GFM alignment via inline `text-align` — the documented exception to the classes-only rule. Styled in `theme.css`, documented in the theme README. Verified with a live Hugo build. ## Contract surfaces (per CLAUDE.md, same change) Go allow-set, TS mirror, `scaffold/site/AGENTS.md` (token named; "omitting allow" rewording to "default set — every token above except `table`"), drift test. ## Verification - `go vet ./...`, `go test ./...` (all packages, fresh run), `go build` — green (CGO_ENABLED=0; this sandbox has no gcc). - web: 816 tests / 40 files green, `vite build` succeeds, `tsc --noEmit` clean (CI parity: npm ci/build/test). - Hugo smoke test of the render hook shows the wrapper div, `<thead>`, and alignment styles in emitted HTML. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(rte): opt-in 'table' token with GFM-constrained round-trip
All checks were successful
ci / build-and-test (pull_request) Successful in 7m8s
d731c6387f
Add 'table' as a ninth rte formatting token, opt-in only: the default
allow-set stays the current 8 tokens and 'table' must be named
explicitly in an element's allow list. The scaffolded text element opts
in.

Engine: the valid token vocabulary (rteAllowTokens) is decoupled from
the default set (defaultRTEAllow); the server body gate moves tables
from never-allowed to allow-gated while still policing cell contents
(raw HTML, marks) — the no-raw-HTML defense is untouched.

Editor: tiptap table extensions (2.27.2) load only when the token is
granted, GFM-constrained — inline-only cells, inert merge/split and
header-toggle commands, first row is structurally the header, no column
resizing. markdown-it's core table rule is enabled on opt-in and a
canonical pipe-table serializer round-trips agent-authored tables
byte-equivalently (alignment colons preserved via a per-cell textAlign
attr; pipes escaped; hard breaks in cells degrade to a space; degenerate
shapes repaired to valid GFM, never HTML). No creation UI ships in this
slice.

Theme: a table render hook wraps every rendered markdown table in a
keyboard-focusable horizontal-scroll container (ce-table), honoring GFM
column alignment; styled in theme.css and documented in the README.

Contract surfaces updated together per CLAUDE.md: Go allow-set, TS
mirror (web/src/rte/allow.ts), scaffold/site/AGENTS.md (default-set
wording now 'all tokens except table'), and the agentsdoc drift test now
iterates the full vocabulary.

Closes #94

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

This was generated by AI while landing a PR.

Validation verdict: PASS.

Verification signal relied on: the green Forgejo Actions run ci / build-and-test (success, 7m8s) — full go vet / go test ./... / go build and the web vitest suite, including the round-trip identity table in test/rte/markdown.test.ts and the server allow-gating tests. Not re-run.

Checked:

  • Merges cleanly into current main (base had moved; no conflict).
  • Title is Conventional Commits (feat(rte): …); body carries Closes #94 on afk/94.
  • Default allow-set unchanged (8 tokens); rteAllowTokens/ALL_RTE_TOKENS add table as opt-in-only vocabulary. Elements omitting allow never gain tables, server- and client-side.
  • text.yaml opts in explicitly (8 defaults + table).
  • Server (body.go) allow-gates tables and still descends into cells to police inline content (raw HTML in a cell still rejected).
  • Tiptap extension is GFM-constrained: inline-only cells, merge/split/header-toggle inert, no column resizing, row 1 is the header.
  • Serializer emits canonical single-space-padded GFM with header-derived alignment; pipes escaped, in-cell hard breaks degrade to a space, colspan/ragged rows repaired to valid GFM.
  • Contract surfaces per CLAUDE.md all updated: Go set, TS mirror, scaffold/site/AGENTS.md, and the agentsdoc_test.go drift test (now iterating the full vocabulary).

No blockers. Awaiting a free-text merge go-ahead before merging.

> *This was generated by AI while landing a PR.* **Validation verdict: PASS.** Verification signal relied on: the green Forgejo Actions run `ci / build-and-test` (success, 7m8s) — full `go vet` / `go test ./...` / `go build` and the web vitest suite, including the round-trip identity table in `test/rte/markdown.test.ts` and the server allow-gating tests. Not re-run. Checked: - Merges cleanly into current `main` (base had moved; no conflict). - Title is Conventional Commits (`feat(rte): …`); body carries `Closes #94` on `afk/94`. - Default allow-set unchanged (8 tokens); `rteAllowTokens`/`ALL_RTE_TOKENS` add `table` as opt-in-only vocabulary. Elements omitting `allow` never gain tables, server- and client-side. - `text.yaml` opts in explicitly (8 defaults + `table`). - Server (`body.go`) allow-gates tables and still descends into cells to police inline content (raw HTML in a cell still rejected). - Tiptap extension is GFM-constrained: inline-only cells, merge/split/header-toggle inert, no column resizing, row 1 is the header. - Serializer emits canonical single-space-padded GFM with header-derived alignment; pipes escaped, in-cell hard breaks degrade to a space, colspan/ragged rows repaired to valid GFM. - Contract surfaces per CLAUDE.md all updated: Go set, TS mirror, `scaffold/site/AGENTS.md`, and the `agentsdoc_test.go` drift test (now iterating the full vocabulary). No blockers. Awaiting a free-text merge go-ahead before merging.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!102
No description provided.