Enforce the XHTML Strict content model for block/inline nesting#13
Merged
Conversation
The editor's flat-block model neither allowed everything the XHTML DTD allows nor forbade everything it forbids. Nesting decisions now flow from a single declarative source of truth: - content-model.xsl (new): 1:1 transcription of the XHTML 1.0 Strict DTD (Appendix A.1 entities + element declarations) and the Appendix B prohibitions, extended with HTML5 figure/figcaption; cm:* predicates consumed by edit, canonical and lint so verdicts cannot drift - canonical-xhtml.xsl: two passes - sanitize/normalize (content-aware div handling, run-wrapper and sectioning unwrap), then cm-normalize (blockquote stray runs wrapped in p, blocks hoisted out of inline-only elements RDFa-preservingly, ul/dl/tr child coercion, table fostering, a-in-a and pre exclusions); region-children block coercion at entry - edit.xsl: recursive editability init (text host vs container per the model, at any depth); mixed flow content like <li>text<ul>...</ul></li> gets an ephemeral p.rdfa-editor-run host and round-trips byte-identical; invalid host markup normalized once at load; paste nests blocks inside flow hosts instead of flattening; toolbar inserts follow the caret context (a list inserted in a cell nests in the cell) - authoring: Tab/Shift+Tab indent/outdent nested lists, progressive Enter/Backspace exits, quote toggle wraps/unwraps blockquote (block-only per Strict; RDFa-bearing quotes refuse), host-based block-type select, container-aware merges, flow-cell caret landings - lint-xhtml.xsl (new): invalid-nesting, stray-text, prohibited-nesting, unknown-element, surfaced live with the RDFa lint Storage contract change: stored bare-text blockquotes are rewritten to blockquote > p once (blockquote is block-only under Strict). New suites: tests/browser/nesting.mjs + authoring.mjs against tests/fixture-nesting.html; canonical fixtures 13-20, lint fixtures 11-12. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…B4c) The first item of a list nested in a cell (or quote, dd) hit B5's top-level-inert rule, so the list could not be removed with Backspace. New B4c branch: the item merges into the last editable host of the list's preceding sibling inside the container, the emptied list is removed and the container collapses back to a plain text host; the caret rides a surviving text-node reference through both the merge and the collapse unwrap. Composites (table, figure) stay hard boundaries; a list with nothing mergeable before it stays inert (B5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s-product Hand-picked scenario tests missed gesture/context combinations (a list inserted in a cell, run-wrapper editability) - the space is too big to enumerate. invariants.mjs runs a uniform battery (type, Enter, Backspace, Tab/Shift+Tab, list/paragraph insert, quote toggle, convert, block paste) in every caret context (top-level, quote paragraph, leaf/container/nested list items, leaf and container cells, li-in-td, figcaption) and asserts properties that hold regardless of outcome: no text outside an editable host, no nested hosts, chrome top-level only, run wrappers editable, zero lint issues, and undo unwinding to the exact post-init baseline. fixture-nesting.html gains a figure block for the figcaption context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness fixes, each verified by an adversarial review pass and covered by new regression tests (authoring.mjs section F, canonical fixtures 21-22, embedded-region fixture): - Region-boundary clamps: local:item-of, the Tab cell lookup and the quote-toggle blockquote lookup now require [exists(local:block-of(.))], so a host-page li/td/blockquote wrapping an embedded region is never reparented, traversed, grown or dissolved by editor gestures - Composite hard boundaries on Backspace merges: new local:merge-host-in refuses a target inside a table/figure nested at the previous item's/container's tail (B4, B2b, B4c) - the gesture stays inert - N1 rewritten to reach the fixed point in one bottom-up pass (decides on the PROCESSED children), split shells keep ALL attributes (an <a href> split by a block no longer loses its target), and the RDFa-preserving branch demotes blocks recursively via mode="cm-demote" (a list becomes nested spans, never a bare li inside a span) - cm:wrap-inline-runs pulls only text and known-inline elements into its inline-only wrapper; known non-inline strays (li outside a list) pass through bare for lint instead of becoming manufactured invalid nesting - E4b anchors the list-exit paragraph before removing the list, so exiting a sole-item list nested in a cell stays inside the cell (no more dead cell + caret teleport to region end) - E6 anchors the caption-exit paragraph after the figure/table itself, which may be nested inside an item or cell - A focused image island deletes its figure when it has one, else just the image - never the whole containing list/table; the emptied container collapses back to a text host - convert-block's caret guard compares local:host-of (callers pass nested hosts), so converting a quote paragraph keeps the caret - insert-block-at-caret treats figure as a locked composite: inserting at a figcaption nests inside the caption, mirroring paste - content-model.xsl fidelity: form uses %form.content; (no nested form), isindex added to the Appendix B button prohibitions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both phases plus the review fixes are shipped; CLAUDE.md carries the living module contracts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the editor true to the XHTML 1.0 Strict content model (extended with HTML5
figure/figcaption): every nesting the DTD allows is authorable and round-trips intact; every nesting it disallows is normalized at boundaries and reported by lint.src/content-model.xsl(new) — 1:1 transcription of the Strict DTD (Appendix A.1 entities + element declarations) and Appendix B prohibitions;cm:*predicates are the single source of truth consumed by edit, canonical and lint, so verdicts cannot driftsrc/canonical-xhtml.xsl— two passes: sanitize/normalize, thenmode="cm-normalize"coerces to the content model (blockquote stray runs →p, blocks hoisted out of inline-only elements RDFa-preservingly, list/dl/tr coercion, table fostering, Appendix B fixes), reaching the fixed point in one bottom-up passsrc/edit.xsl— recursive editability init (text host vs container per the model, at any depth); mixed flow content (<li>text<ul>…</ul></li>) gets an ephemeralp.rdfa-editor-runhost and round-trips byte-identical; paste nests blocks inside flow hosts; toolbar inserts follow the caret contextsrc/lint-xhtml.xsl(new) —invalid-nesting,stray-text,prohibited-nesting,unknown-element, surfaced live with the RDFa lintStorage contract change
blockquoteis block-only under Strict: stored bare-text quotes are rewritten toblockquote > ponce at load/save. Host apps (LDH) with stored bare-text quotes will see documents change shape on first edit.Review
An 8-angle adversarial review ran pre-merge; 10 confirmed findings (region-boundary escapes, composite-boundary merge holes, normalization fixed-point/attribute-loss defects, container-blind gestures, DTD transcription fidelity) are fixed in the final commit, each with a regression test.
Testing
liround-trip acceptance)nesting.mjs(foundation),authoring.mjs(gestures + review regressions) andinvariants.mjs(uniform gesture battery × 9 caret contexts asserting validity/editability/undo invariants)🤖 Generated with Claude Code