Skip to content

Cross-renderer verification against independent HTML sources - #71

Merged
jpcamara merged 3 commits into
mainfrom
feat/cross-render-verification
Aug 20, 2026
Merged

Cross-renderer verification against independent HTML sources#71
jpcamara merged 3 commits into
mainfrom
feat/cross-render-verification

Conversation

@jpcamara

Copy link
Copy Markdown
Owner

A new harness (examples/actioncable-demo/frontend/cross_render.mjs,
wired into CI) renders a shared corpus through the gem and through the
prominent independent implementations:

  • a live headless Tiptap editor's getHTML: byte parity, the gem's
    existing contract (the table case strips the documented editor-view
    chrome: colgroup and min-width styles)
  • @tiptap/html's static generateHTML: DOM-normalized, with its two
    verified quirks allowlisted (drops textStyle color, writes
    data-checked="" for checked items)
  • tiptap-php via docker when available, with the full extension set
    configured and its spellings translated ( for , explicit
    start="1"); its dropped textStyle attributes are allowlisted
  • Lexical's $generateHtmlFromNodes on a headless editor bound over
    @lexical/yjs, with stock export idioms stripped (white-space spans,
    and doubling, spellcheck attributes)

Every comparison converges to zero differences, so any new divergence
from any source fails the run.

The harness immediately found a real bug: adjacent text runs sharing
marks rendered as sibling wrappers (ab
) instead of keeping shared marks open
(ab) the way ProseMirror's serializer does.
The fix builds each run's ordered open/close stack and diffs
consecutive stacks; sharing is by identical opening tag, so a mark
with different attributes never merges. Pinned by a crate test and by
the mark-matrix corpus case against all sources. The captured fixtures
never contained adjacent shared-prefix runs, which is how it hid.

prosemirror-yjs-html bumps to 0.1.2. render_check.rb gains
prosemirror-core and lexical-core kinds so the harness can compare the
core renderers.

A new harness renders a shared corpus through the gem and through the
prominent independent implementations: a live headless Tiptap editor
(byte parity, the gem's contract), @tiptap/html's static generateHTML,
tiptap-php in docker when available, and Lexical's
$generateHtmlFromNodes on a headless editor bound over @lexical/yjs.
Comparisons beyond the byte-parity contract are DOM-normalized, with
each source's documented spelling quirks translated or allowlisted so
anything new still fails. Runs in CI after the render-parity e2e.

It found a real bug: adjacent text runs sharing marks rendered as
sibling wrappers instead of keeping the shared marks open, the way
ProseMirror's serializer does. run_stack now builds each run's ordered
open/close stack and render_text_runs diffs consecutive stacks, closing
and opening only the tails. A mark whose attributes differ between
runs never merges, since sharing is by identical opening tag. The
prosemirror-yjs-html crate bumps to 0.1.2 with the fix; a crate test
pins the merge.
The merging emitter replaced render_run's production call site, leaving
it test-only; clippy under -D warnings rejects the dead public path.
Add @payloadcms/richtext-lexical's convertLexicalToHTML as a fifth
cross-renderer source, over the same headless editor's serialized state.
Link nodes are re-shaped into Payload's fields dialect first. A custom
converter covers the code-block node, which Payload's default set does
not. Payload's own spellings — styled spans for strike and underline,
em over strong for bold italic, list classes, empty li attributes — are
translated before the DOM-normalized compare, and anything else fails.

Peer auto-install is now off for the frontend (bunfig.toml). Payload's
admin-UI peers (react, next) are dead weight for the converter. The
peers the demo really uses are explicit devDependencies instead:
payload, @tiptap/y-tiptap, @tiptap/suggestion. @lexical/table and
@lexical/text are pinned at 0.44 so Payload's nested 0.41 copies do
not win the hoist.
@jpcamara
jpcamara merged commit 63233ed into main Aug 20, 2026
7 checks passed
@jpcamara
jpcamara deleted the feat/cross-render-verification branch August 20, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant