From 10d0f5c4665f534bbf573582f151ad7866d78e6a Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 30 Sep 2026 09:15:20 -0400 Subject: [PATCH 1/2] fix(php-transformer): preserve empty destination anchors as native links --- php-transformer/composer.json | 2 + .../Elements/ButtonLinkDispatcher.php | 10 ++++ .../tests/unit/button-link-dispatcher.php | 4 +- .../tests/unit/empty-destination-anchor.php | 44 +++++++++++++++++ .../unit/empty-named-fragment-target.php | 4 +- .../tests/empty-destination-anchor.mjs | 49 +++++++++++++++++++ 6 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 php-transformer/tests/unit/empty-destination-anchor.php create mode 100644 php-transformer/tools/visual-parity/tests/empty-destination-anchor.mjs diff --git a/php-transformer/composer.json b/php-transformer/composer.json index ad0becd52..61e05d06f 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -41,6 +41,7 @@ "static-parity": "php tools/static-parity/run.php", "live-wp-parity": "php tools/live-wp-parity/run.php", "visual:image-custom-host-promotion": "node tools/visual-parity/tests/image-custom-host-promotion.mjs", + "visual:empty-destination-anchor": "node tools/visual-parity/tests/empty-destination-anchor.mjs", "benchmark:selector-cache": "php tools/benchmarks/css-selector-match-cache.php", "benchmark:staged-page-compilation": "php tools/benchmarks/staged-page-compilation-production.php", "corpus-diagnostics": "php tools/corpus-diagnostics/run.php", @@ -377,6 +378,7 @@ "php tests/unit/runtime-island-analyzer.php", "php tests/unit/commerce-structure-recognizer.php", "php tests/unit/button-link-dispatcher.php", + "php tests/unit/empty-destination-anchor.php", "php tests/unit/button-element-converter.php", "php tests/unit/block-content-button-not-richtext.php", "php tests/unit/image-carrier-hidden-label.php", diff --git a/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php b/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php index 6f59451ba..3747c7888 100644 --- a/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php +++ b/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php @@ -72,6 +72,16 @@ public function convertAnchor(DOMElement $element, array &$fallbacks): ?array return $this->paragraphHost($element); } + // A childless whitespace-only link still owns its destination and may + // own a CSS hitbox. Use the same native RichText host as other links; + // neither a duplicate sibling destination nor absent text proves that + // it is safe to omit. Keep media-bearing anchors on their own lowering. + if ( 0 === SourceDom::childElementCount($element) + && '' === trim($element->textContent ?? '') + && '' !== $this->context->safeLinkUrl(SourceDom::attr($element, 'href')) ) { + return $this->paragraphHost($element); + } + // A text-less anchor is not an empty anchor: icons, one image, a whole // image feed, a media carrier — whatever it wraps is content, and the // link-wrapper group is the lowering that keeps it while propagating the diff --git a/php-transformer/tests/unit/button-link-dispatcher.php b/php-transformer/tests/unit/button-link-dispatcher.php index 58ad65062..a339c1b22 100644 --- a/php-transformer/tests/unit/button-link-dispatcher.php +++ b/php-transformer/tests/unit/button-link-dispatcher.php @@ -129,8 +129,8 @@ static function (DOMElement $e, string $kind) use (&$islandRecorded): void { // A plain button that matches no pattern yields nothing. $assert(null === $makeDispatcher()->convertButton($elementFrom('')), 'unmatched-button-yields-nothing'); -// An empty anchor with no accessible name drops. -$assert(null === $makeDispatcher()->convertAnchor($elementFrom(''), $fallbacks), 'empty-anchor-drops'); +// An empty safe link retains its destination on a native RichText host. +$assert('core/paragraph' === ($makeDispatcher()->convertAnchor($elementFrom(''), $fallbacks)['blockName'] ?? ''), 'empty-anchor-retains-destination'); // Untitled inline SVG is visual content. Convert children instead of dropping. $svgChildren = $makeDispatcher(array( diff --git a/php-transformer/tests/unit/empty-destination-anchor.php b/php-transformer/tests/unit/empty-destination-anchor.php new file mode 100644 index 000000000..56baa3db2 --- /dev/null +++ b/php-transformer/tests/unit/empty-destination-anchor.php @@ -0,0 +1,44 @@ + '
Pencil Plant\n \n
', + 'accessible overlay' => '
Tickets available
', + 'positive geometry' => '', + 'unique destination' => ' ', +); +$cases['redundant whitespace sibling'] = str_replace('\\n', "\n", $cases['redundant whitespace sibling']); +$css = ''; +foreach ($cases as $label => $source) { + $result = (new HtmlTransformer())->transform($css . '
' . $source . '
')->toArray(); + $markup = $result['serialized_blocks']; + $assert(array() === $result['fallbacks'], "$label has no unsupported fallback"); + $assert(! str_contains($markup, '