Extract text-leaf element conversion out of convertElement (#242) - #1310
Merged
Conversation
address, noscript, marquee/blink, pre, plaintext, hr, and br move from the convertElement dispatch chain into TextLeafElementConverter, which receives an explicit TextLeafElementContext instead of transformer $this. Uses a collaborator, not a trait: a single-consumer trait moves code across a file boundary while leaving every method in the transformer's object scope, so it does not reduce the surface this epic is about. ConversionOutcome keeps "converted to nothing" distinct from "not my tag", which is what let these branches leave the chain without changing dispatch order.
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.
First decomposition slice for #242, workstream 1.
Why a collaborator and not a trait
The recent extractions moved code into traits that
HtmlTransformeris the only consumer of. Measured on trunk before this PR: 8 of the 9 traits mixed into the transformer have exactly one consumer, totalling 8,887 lines. A single-consumer trait changes the file listing, not the$thissurface — every method still shares one mutable object scope, so coupling, testability, and parallel-agent collision safety are unchanged.TextLeafElementConverteris a plain object. It receivesTextLeafElementContext(12 explicit operations, mirroring the existingPatternContextidiom) and has no access to transformer state that is not on that list.What moved
address,noscript,marquee/blink,pre,plaintext,hr,br— the branches inconvertElement()whose mapping depends only on the element's own content and presentation attributes.ConversionOutcomeexists because the dispatch chain overloadsnullto mean both "this element intentionally produces no block" (br) and "this branch did not apply, keep dispatching". Collapsing those into a bare?arrayis part of what keeps branches pinned inline. Separating them is what let these leave the chain without reordering dispatch.Behavior preservation
The dispatch chain is order-sensitive, so each extracted branch is invoked from exactly the position it previously occupied.
Proof is a corpus hash diff, not just a green suite. Before any edit, every HTML fixture under
fixtures/was transformed on cleanorigin/trunkand itsserializedBlocksSHA-256 recorded alongside block/diagnostic/fallback counts. Same capture after:composer testexit 0 — 289 parity fixtures, contract suite, unit suite, packaging proof.tests/unit/text-leaf-element-converter.php: 31 assertions, registered intest:unit.Impact
HtmlTransformer.phpconvertElement()The line delta is deliberately small — 54 lines of
convertElement()and ~25 net off the class. What matters is that those conversions are no longer reachable through$this, and are now covered by a unit test that constructs no transformer. Previously, covering them required driving a full document through the pipeline.Scope
This is slice 1.
convertElement()is still a 387-line ordered chain andHtmlTransformeris still ~12.6k lines. Follow-on slices in the same shape: the heading/paragraph rich-text branches (they need the richtext/SVG helper cluster), the table branch (TableClassificationPolicyis already a collaborator), and the terminalcaptureUnsupportedfallback block.Suggested metric for the remaining slices, per the audit comment above: count methods no longer reachable through
$this, not lines removed from the file.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 the extraction, verified byte-identical
serializedBlocksoutput across 385 fixtures, and wrote the isolation test. Reviewed by a human before opening.