From 419adb95bb7848e6bd24d989618cb1a0dbfc7dd0 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 9 Nov 2026 12:47:29 -0500 Subject: [PATCH] refactor(php-transformer): move inert element recognition into Elements/InertElements Moves isInertRuntimeMediaPlaceholder, isInertHiddenSvgStorage and hiddenSvgStoreHasNonSvgConsumer out of HtmlCompilation verbatim. --- .../HtmlToBlocks/Elements/InertElements.php | 123 ++++++++++++++++++ .../src/HtmlToBlocks/HtmlCompilation.php | 105 +-------------- 2 files changed, 129 insertions(+), 99 deletions(-) create mode 100644 php-transformer/src/HtmlToBlocks/Elements/InertElements.php diff --git a/php-transformer/src/HtmlToBlocks/Elements/InertElements.php b/php-transformer/src/HtmlToBlocks/Elements/InertElements.php new file mode 100644 index 000000000..3033dc6c2 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/Elements/InertElements.php @@ -0,0 +1,123 @@ +sourceElementClassifier->isRuntimeMediaSurfaceElement($element) + || ! str_contains(strtolower($element->tagName), '-') + || '' !== trim($element->textContent ?? '') + ) { + return false; + } + + foreach ( array_merge(array( $element ), iterator_to_array($element->getElementsByTagName('*'))) as $candidate ) { + if ( ! $candidate instanceof DOMElement + || ! $this->runtimeIslands->isSafeTransparentCustomElement($candidate) + || '' !== trim(SourceDom::attr($candidate, 'role')) + || '' !== trim(SourceDom::attr($candidate, 'aria-label')) + ) { + return false; + } + + foreach ( array( 'src', 'srcset', 'href', 'data-src', 'data-url' ) as $attribute ) { + if ( '' !== trim(SourceDom::attr($candidate, $attribute)) ) { + return false; + } + } + + foreach ( $candidate->attributes as $attribute ) { + if ( str_starts_with(strtolower($attribute->name), 'data-') && '' !== trim($attribute->value) ) { + return false; + } + } + + if ( $candidate !== $element && ! $this->sourceElementClassifier->isStructuralTransparentCustomWrapperChild($candidate) ) { + return false; + } + } + + return true; + } + + public function isInertHiddenSvgStorage(DOMElement $element): bool + { + $isDomStore = $element->hasAttribute('data-dom-store'); + if ( ! $this->sourceVisibility->startsHidden($element) + || (! $isDomStore && ( + $this->styleResolver->hasConditionalStyleFamily($element, 'layout') + || $this->styleResolver->hasConditionalStyleFamily($element, 'visibility') + || $this->styleResolver->hasConditionalStyleFamily($element, 'opacity') + )) + || '' !== trim(SourceDom::attr($element, 'aria-label')) + || '' !== trim(SourceDom::attr($element, 'aria-labelledby')) + || '' !== trim(SourceDom::attr($element, 'title')) + || ! $this->sourceElementClassifier->hasOnlySvgDefinitions($element) + || $this->hiddenSvgStoreHasNonSvgConsumer($element) ) { + return false; + } + + return true; + } + + private function hiddenSvgStoreHasNonSvgConsumer(DOMElement $store): bool + { + $ids = array(); + foreach ( $store->getElementsByTagName('*') as $definition ) { + if ( $definition instanceof DOMElement && '' !== trim(SourceDom::attr($definition, 'id')) ) { + $ids[] = trim(SourceDom::attr($definition, 'id')); + } + } + if ( array() === $ids || ! $store->ownerDocument instanceof DOMDocument ) { + return false; + } + + foreach ( $store->ownerDocument->getElementsByTagName('*') as $candidate ) { + if ( ! $candidate instanceof DOMElement || $this->sourceElementClassifier->isDescendantOf($candidate, $store) ) { + continue; + } + foreach ( $candidate->attributes as $attribute ) { + foreach ( $ids as $id ) { + if ( preg_match('/(?:^|[\\s,(])(?:url\\(\\s*["\']?#' . preg_quote($id, '/') . '["\']?\\s*\\)|#' . preg_quote($id, '/') . '(?:$|[\\s,)]))/', $attribute->value) + && 'svg' !== strtolower($candidate->tagName) + && null === SourceDom::ancestorElement($candidate, 'svg') ) { + return true; + } + } + } + if ( 'style' === strtolower($candidate->tagName) ) { + foreach ( $ids as $id ) { + if ( preg_match('/url\\(\\s*["\']?#' . preg_quote($id, '/') . '["\']?\\s*\\)/', $candidate->textContent ?? '') ) { + return true; + } + } + } + } + + return false; + } +} diff --git a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php index f8bd86489..91e3451e1 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php +++ b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php @@ -314,6 +314,7 @@ final class HtmlCompilation extends SourceBlockCreator implements RichTextInline private readonly Elements\CustomImageHostLowering $customImageHosts; private readonly Elements\AnchorLowering $anchors; private readonly Elements\RenderNeutralElements $renderNeutral; + private readonly Elements\InertElements $inertElements; private readonly SourceProvenance $sourceProvenance; private readonly RuntimeResourceElementConverter $runtimeResourceConverter; @@ -605,6 +606,7 @@ function (DOMElement $element): array { $this->listStructure ); $this->renderNeutral = new Elements\RenderNeutralElements($this->session, $this->styleResolver, $this->sourceElementClassifier, $this->sourceElementDescription, $this->runtimeIslands, $this->emptyElementFootprint); + $this->inertElements = new Elements\InertElements($this->styleResolver, $this->sourceElementClassifier, $this->sourceVisibility, $this->runtimeIslands); $this->customImageHosts = new Elements\CustomImageHostLowering($this, $this->blockFactory, $this->styleResolver, $this->session, $this->sourceElementClassifier, $this->sourceElementDescription, $this->imageLowering, $this->layoutShells, $this->runtimeIslands, $this->authorLayoutOwnership); $this->anchors = new Elements\AnchorLowering($this, $this, $this->styleResolver, $this->session, $this->sourceElementClassifier, $this->sourceElementDescription, $this->imageLowering, $this->imageUrls, $this->layoutShells, $this->linkWrapperPropagator, $this->runtimeIslands, $this->customImageHosts); $this->layoutTables = new Elements\LayoutTableLowering( @@ -645,7 +647,7 @@ function (DOMElement $element): array { $this->textLeafConverter = new TextLeafElementConverter($this->richTextMaterializer, $this->runtime, $this, $this->styleResolver, $this->sourceElementClassifier, $this->createTextLeafElementContext()); $this->richTextConverter = new RichTextElementConverter($this, $this->styleResolver, $this->richTextMaterializer, $this->runtime, $this->createRichTextElementContext()); $svgConverter = new SvgElementConverter($this, $this->sourceElementClassifier, $this->styleResolver, new SvgElementContext( - fn (DOMElement $element): bool => $this->isInertHiddenSvgStorage($element), + fn (DOMElement $element): bool => $this->inertElements->isInertHiddenSvgStorage($element), fn (DOMElement $element): bool => $this->runtimeIslands->isRuntimeDomTarget($element), fn (DOMElement $element): string => $this->sanitizeInlineSvgMarkup($element), fn (string $html): bool => $this->isSafeSvgContent($html), @@ -954,7 +956,7 @@ function (DOMElement $element, array &$fallbacks): array { $this, $this->runtimeIslands, $this->session, - fn (DOMElement $element): bool => $this->isInertRuntimeMediaPlaceholder($element), + fn (DOMElement $element): bool => $this->inertElements->isInertRuntimeMediaPlaceholder($element), fn (DOMElement $element): bool => $this->sourceVisibility->startsHidden($element), $this->sourceElementDescription ); @@ -1094,7 +1096,7 @@ function (DOMElement $element, array &$fallbacks): ?array { fn (DOMElement $element): bool => $this->shouldPreserveEmptyVisualElement($element) ), $this->projectedNavigation, - new PhrasingSvgConverter($this->svgMaterializer, $this, fn (DOMElement $element): bool => $this->isInertHiddenSvgStorage($element)), + new PhrasingSvgConverter($this->svgMaterializer, $this, fn (DOMElement $element): bool => $this->inertElements->isInertHiddenSvgStorage($element)), $capturedDialogConverter, new CapturedChoiceGroupConverter( $this->session, @@ -3566,7 +3568,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca return $transparentCustomElement; } - if ( $this->isInertRuntimeMediaPlaceholder($element) ) { + if ( $this->inertElements->isInertRuntimeMediaPlaceholder($element) ) { return null; } @@ -3799,44 +3801,6 @@ private function recognizedNavigationHostBlock(DOMElement $element, array &$fall } - private function isInertRuntimeMediaPlaceholder(DOMElement $element): bool - { - if ( ! $this->sourceElementClassifier->isRuntimeMediaSurfaceElement($element) - || ! str_contains(strtolower($element->tagName), '-') - || '' !== trim($element->textContent ?? '') - ) { - return false; - } - - foreach ( array_merge(array( $element ), iterator_to_array($element->getElementsByTagName('*'))) as $candidate ) { - if ( ! $candidate instanceof DOMElement - || ! $this->runtimeIslands->isSafeTransparentCustomElement($candidate) - || '' !== trim($this->attr($candidate, 'role')) - || '' !== trim($this->attr($candidate, 'aria-label')) - ) { - return false; - } - - foreach ( array( 'src', 'srcset', 'href', 'data-src', 'data-url' ) as $attribute ) { - if ( '' !== trim($this->attr($candidate, $attribute)) ) { - return false; - } - } - - foreach ( $candidate->attributes as $attribute ) { - if ( str_starts_with(strtolower($attribute->name), 'data-') && '' !== trim($attribute->value) ) { - return false; - } - } - - if ( $candidate !== $element && ! $this->sourceElementClassifier->isStructuralTransparentCustomWrapperChild($candidate) ) { - return false; - } - } - - return true; - } - /** * @return array|null */ @@ -6883,63 +6847,6 @@ private function containsExcludedMedia(DOMElement $element): bool return false; } - private function isInertHiddenSvgStorage(DOMElement $element): bool - { - $isDomStore = $element->hasAttribute('data-dom-store'); - if ( ! $this->sourceVisibility->startsHidden($element) - || (! $isDomStore && ( - $this->styleResolver->hasConditionalStyleFamily($element, 'layout') - || $this->styleResolver->hasConditionalStyleFamily($element, 'visibility') - || $this->styleResolver->hasConditionalStyleFamily($element, 'opacity') - )) - || '' !== trim($this->attr($element, 'aria-label')) - || '' !== trim($this->attr($element, 'aria-labelledby')) - || '' !== trim($this->attr($element, 'title')) - || ! $this->sourceElementClassifier->hasOnlySvgDefinitions($element) - || $this->hiddenSvgStoreHasNonSvgConsumer($element) ) { - return false; - } - - return true; - } - - private function hiddenSvgStoreHasNonSvgConsumer(DOMElement $store): bool - { - $ids = array(); - foreach ( $store->getElementsByTagName('*') as $definition ) { - if ( $definition instanceof DOMElement && '' !== trim($this->attr($definition, 'id')) ) { - $ids[] = trim($this->attr($definition, 'id')); - } - } - if ( array() === $ids || ! $store->ownerDocument instanceof DOMDocument ) { - return false; - } - - foreach ( $store->ownerDocument->getElementsByTagName('*') as $candidate ) { - if ( ! $candidate instanceof DOMElement || $this->sourceElementClassifier->isDescendantOf($candidate, $store) ) { - continue; - } - foreach ( $candidate->attributes as $attribute ) { - foreach ( $ids as $id ) { - if ( preg_match('/(?:^|[\\s,(])(?:url\\(\\s*["\']?#' . preg_quote($id, '/') . '["\']?\\s*\\)|#' . preg_quote($id, '/') . '(?:$|[\\s,)]))/', $attribute->value) - && 'svg' !== strtolower($candidate->tagName) - && null === SourceDom::ancestorElement($candidate, 'svg') ) { - return true; - } - } - } - if ( 'style' === strtolower($candidate->tagName) ) { - foreach ( $ids as $id ) { - if ( preg_match('/url\\(\\s*["\']?#' . preg_quote($id, '/') . '["\']?\\s*\\)/', $candidate->textContent ?? '') ) { - return true; - } - } - } - } - - return false; - } - /** * Whether an image's wrapping link is a lightbox trigger rather than a * destination: it has to point at an image resource and be marked as a