Extract table routing and the unsupported-element fallback (#242) - #1315
Merged
Conversation
TableElementConverter owns the table branch, where ordering is the contract: layout-table shapes are claimed before data-table classification, so a table used for layout becomes columns rather than a semantic core/table. TableClassificationPolicy was already a collaborator and is injected directly instead of wrapped in closures. UnsupportedElementRecorder owns the terminal core/html fallback decision (issue #497) that closes convertElement.
This was referenced Aug 28, 2026
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.
Slice 3 for #242, workstream 1. Follows #1310 and #1312.
What moved
TableElementConverter— thetablebranch. Ordering is the contract here: layout-table shapes (isNestedLayoutTableMember,isMediaLayoutTable,isPercentLayoutTable) are claimed before the element reaches data-table classification, so a table used purely for layout becomes columns rather than a semanticcore/table. That precedence is now asserted directly rather than implied by statement order in a 300-line method.TableClassificationPolicywas already a first-class collaborator, so it is injected as itself instead of wrapped in closures — only the transformer-owned block builders are passed as closures.UnsupportedElementRecorder— the terminalcaptureUnsupportedblock that closesconvertElement(). This is the core/html fallback decision from issue #497: generate a static-render custom block when the structural classifier is confident, otherwise record anhtml_unsupported_elementdiagnostic and drop the element.Behavior preservation
Corpus hash diff on clean
trunkbefore any edit, recaptured after —serializedBlocksSHA-256 plus block/diagnostic/fallback counts for every fixture HTML file:The fallback counts matter for this slice specifically:
UnsupportedElementRecorderappends diagnostics, so an identical fallback count across the corpus is what shows the diagnostic path is unchanged, not just the block output.composer testexit 0 — 289 parity fixtures, contract suite, unit suite, packaging proof.Coverage
Two new isolation tests, neither constructs a transformer:
tests/unit/table-element-converter.php— 13 assertions. Covers that layout projection precedes preservation (a nested table is unrepresentable by classification, but the layout branch claims it first and yields columns), and that a spanning table is preserved rather than lossily flattened.tests/unit/unsupported-element-recorder.php— 15 assertions. Covers that a generated custom block short-circuits the fallback entirely with no diagnostic recorded, the full diagnostic row shape, and thatcontrolmetadata is attached only when the element actually has it.Impact
HtmlTransformer.phpconvertElement()The class grew by 2 lines — the two context factories cost slightly more than the extracted branches saved. That is the honest number and it is the point: this epic is not about shrinking the file, it is about removing methods from the transformer's
$thisscope and making them testable.Cumulative across the three slices:
convertElement()441 → 301, with 9 collaborator files and 100 isolation assertions that construct no transformer.Scope
convertElement()is still a 301-line ordered chain. What remains inline is mostly guard clauses and pattern-recognizer delegations that are already thin; the next meaningful targets are the media/anchor/inline/list/figureconvertXDispatchElementmethods, which are separate methods but still trait- or$this-bound.AI assistance disclosure: implemented and drafted by Claude Sonnet 4.6 running in Claude Code, operated by @chubes4. The AI captured the pre-change corpus baseline, performed both extractions, verified byte-identical
serializedBlocksand fallback counts across 385 fixtures, and wrote the isolation tests. Reviewed by a human before opening.