Skip to content

Extract text-leaf element conversion out of convertElement (#242) - #1310

Merged
chubes4 merged 1 commit into
trunkfrom
refactor/242-convert-element-dispatch
Aug 28, 2026
Merged

Extract text-leaf element conversion out of convertElement (#242)#1310
chubes4 merged 1 commit into
trunkfrom
refactor/242-convert-element-dispatch

Conversation

@chubes4

@chubes4 chubes4 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

First decomposition slice for #242, workstream 1.

Why a collaborator and not a trait

The recent extractions moved code into traits that HtmlTransformer is 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 $this surface — every method still shares one mutable object scope, so coupling, testability, and parallel-agent collision safety are unchanged.

TextLeafElementConverter is a plain object. It receives TextLeafElementContext (12 explicit operations, mirroring the existing PatternContext idiom) 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 in convertElement() whose mapping depends only on the element's own content and presentation attributes.

ConversionOutcome exists because the dispatch chain overloads null to mean both "this element intentionally produces no block" (br) and "this branch did not apply, keep dispatching". Collapsing those into a bare ?array is 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 clean origin/trunk and its serializedBlocks SHA-256 recorded alongside block/diagnostic/fallback counts. Same capture after:

corpus fixtures: 385   DIFFERING: 0   threw: 0
  • composer test exit 0 — 289 parity fixtures, contract suite, unit suite, packaging proof.
  • New tests/unit/text-leaf-element-converter.php: 31 assertions, registered in test:unit.

Impact

before after
HtmlTransformer.php 12,633 12,608
convertElement() 441 387

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 and HtmlTransformer is 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 (TableClassificationPolicy is already a collaborator), and the terminal captureUnsupported fallback 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 serializedBlocks output across 385 fixtures, and wrote the isolation test. Reviewed by a human before opening.

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.
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