From 419adb95bb7848e6bd24d989618cb1a0dbfc7dd0 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 9 Nov 2026 12:47:29 -0500 Subject: [PATCH 1/2] 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 From 6be9dd88efcd98f521b58c9dd07cd3736a28608f Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 9 Nov 2026 12:48:54 -0500 Subject: [PATCH 2/2] refactor(php-transformer): move empty inline geometry lowering into Elements/EmptyInlineGeometryLowering Moves emptyInlineGeometryBlock and its six helpers out of HtmlCompilation verbatim. preservedSourceControlBlock reaches htmlPreservationBlock through the injected SourceBlockCreator. --- .../Elements/EmptyInlineGeometryLowering.php | 157 ++++++++++++++++++ .../src/HtmlToBlocks/HtmlCompilation.php | 129 +------------- 2 files changed, 160 insertions(+), 126 deletions(-) create mode 100644 php-transformer/src/HtmlToBlocks/Elements/EmptyInlineGeometryLowering.php diff --git a/php-transformer/src/HtmlToBlocks/Elements/EmptyInlineGeometryLowering.php b/php-transformer/src/HtmlToBlocks/Elements/EmptyInlineGeometryLowering.php new file mode 100644 index 000000000..8d130e008 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/Elements/EmptyInlineGeometryLowering.php @@ -0,0 +1,157 @@ + */ + private function preservedSourceControlBlock(DOMElement $element): array + { + return $this->authoredStateButtons->fromElement($element) ?? $this->blocks->htmlPreservationBlock($element); + } + + /** + * An empty inline tree whose source CSS still generates a line box or flex + * track is layout, not text and not a raw HTML island. core/group cannot + * keep the source phrasing tags, so the existing layout shell does. + * + * @param array> $fallbacks + * @return array|null + */ + public function emptyInlineGeometryBlock(DOMElement $element, array &$fallbacks): ?array + { + if ( $this->hasPaintedSourceText($element) || $this->emptyElementFootprint->subtreeHasPaintedPseudo($element, $this->session->sourceStyleResolutionState()->pseudoElementRules()) || $this->runtimeIslands->isRuntimeDomTarget($element) ) { + return null; + } + $inner = array(); + $layout = false; + foreach ( SourceDom::elementElementChildren($element) as $child ) { + if ( FormControlClassifier::isControlElement($child) ) { + $inner[] = $this->preservedSourceControlBlock($child); + continue; + } + if ( ! $this->isEmptyInlineGeometryNode($child) || $this->containsExcludedMedia($child) ) { + return null; + } + $layout = $layout || $this->emptyElementFootprint->declaresEmptyInlineLayoutBox($child); + $inner[] = $this->emptyInlineGeometryShell($child); + } + if ( ! $layout || array() === $inner ) { + return null; + } + + return $this->layoutShells->blockForElements(array( $element ), $inner, $element); + } + + private function emptyInlineGeometryShell(DOMElement $element): array + { + $wrappers = array( $element ); + $branch = $element; + while ( true ) { + $children = SourceDom::elementElementChildren($branch); + if ( 1 !== count($children) || ! $this->isEmptyInlineGeometryNode($children[0]) ) { + break; + } + $child = $children[0]; + $wrappers[] = $child; + $branch = $child; + if ( count(SourceDom::elementElementChildren($child)) > 1 ) { + break; + } + } + $inner = array(); + foreach ( SourceDom::elementElementChildren($branch) as $child ) { + if ( $child === (array() === $wrappers ? null : $wrappers[array_key_last($wrappers)]) || ! $this->isEmptyInlineGeometryNode($child) ) { + continue; + } + $inner[] = count(SourceDom::elementElementChildren($child)) > 1 + ? $this->emptyInlineGeometryShell($child) + : $this->layoutShells->blockForElements($this->emptyInlineWrapperChain($child), array(), $child); + } + + return $this->layoutShells->blockForElements($wrappers, $inner, $element); + } + + /** + * @return list + */ + private function emptyInlineWrapperChain(DOMElement $element): array + { + $chain = array( $element ); + $cursor = $element; + while ( true ) { + $children = SourceDom::elementElementChildren($cursor); + if ( 1 !== count($children) || ! $this->isEmptyInlineGeometryNode($children[0]) ) { + break; + } + $child = $children[0]; + if ( '' === trim(SourceDom::attr($child, 'class')) && '' === trim(SourceDom::attr($child, 'style')) && 0 === $child->childElementCount ) { + break; + } + $chain[] = $child; + $cursor = $child; + } + + return $chain; + } + + private function isEmptyInlineGeometryNode(DOMElement $element): bool + { + return $this->sourceElementClassifier->isInlineContentElement(strtolower($element->tagName)) + && ! $this->hasPaintedSourceText($element) + && ! $this->containsExcludedMedia($element) + && ! $this->runtimeIslands->isRuntimeDomTarget($element); + } + + private function hasPaintedSourceText(DOMElement $element): bool + { + if ( '' !== trim($element->textContent ?? '') ) { + return true; + } + foreach ( $element->getElementsByTagName('*') as $node ) { + if ( $node instanceof DOMElement && '' !== trim(SourceDom::attr($node, 'data-text')) ) { + return true; + } + } + + return false; + } + + private function containsExcludedMedia(DOMElement $element): bool + { + foreach ( array_merge(array( $element ), SourceDom::descendantElements($element)) as $node ) { + $tag = strtolower($node->tagName); + if ( in_array($tag, array( 'audio', 'canvas', 'iframe', 'img', 'object', 'picture', 'svg', 'video' ), true) ) { + return true; + } + } + + return false; + } +} diff --git a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php index 91e3451e1..9b2fcd02a 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php +++ b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php @@ -315,6 +315,7 @@ final class HtmlCompilation extends SourceBlockCreator implements RichTextInline private readonly Elements\AnchorLowering $anchors; private readonly Elements\RenderNeutralElements $renderNeutral; private readonly Elements\InertElements $inertElements; + private readonly Elements\EmptyInlineGeometryLowering $emptyInlineGeometry; private readonly SourceProvenance $sourceProvenance; private readonly RuntimeResourceElementConverter $runtimeResourceConverter; @@ -607,6 +608,7 @@ function (DOMElement $element): array { ); $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->emptyInlineGeometry = new Elements\EmptyInlineGeometryLowering($this, $this->session, $this->sourceElementClassifier, $this->emptyElementFootprint, $this->authoredStateButtons, $this->layoutShells, $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( @@ -1336,7 +1338,7 @@ function (DOMElement $element, array &$fallbacks, bool $captureUnsupported): arr return $this->convertChildren($element, $fallbacks, $captureUnsupported); }, function (DOMElement $element, array &$fallbacks): ?array { - return $this->emptyInlineGeometryBlock($element, $fallbacks); + return $this->emptyInlineGeometry->emptyInlineGeometryBlock($element, $fallbacks); }, function (DOMElement $element, array &$fallbacks): ?array { $block = $this->recognizePatterns($element, $fallbacks, array( DetailsPattern::class )); @@ -6660,12 +6662,6 @@ private function isDeclaredRuntimeDomTarget(DOMElement $element): bool return false; } - /** @return array */ - private function preservedSourceControlBlock(DOMElement $element): array - { - return $this->authoredStateButtons->fromElement($element) ?? $this->htmlPreservationBlock($element); - } - /** A bounded phrasing tree with named binary SVG controls is layout plus * editable text, rather than one invalid RichText run. Keep source wrappers * with the existing layout shell and transparent text carriers. @@ -6728,125 +6724,6 @@ private function inlineCheckableControlBlock(DOMElement $element): ?array return $build($element); } - /** - * An empty inline tree whose source CSS still generates a line box or flex - * track is layout, not text and not a raw HTML island. core/group cannot - * keep the source phrasing tags, so the existing layout shell does. - * - * @param array> $fallbacks - * @return array|null - */ - private function emptyInlineGeometryBlock(DOMElement $element, array &$fallbacks): ?array - { - if ( $this->hasPaintedSourceText($element) || $this->emptyElementFootprint->subtreeHasPaintedPseudo($element, $this->session->sourceStyleResolutionState()->pseudoElementRules()) || $this->runtimeIslands->isRuntimeDomTarget($element) ) { - return null; - } - $inner = array(); - $layout = false; - foreach ( SourceDom::elementElementChildren($element) as $child ) { - if ( FormControlClassifier::isControlElement($child) ) { - $inner[] = $this->preservedSourceControlBlock($child); - continue; - } - if ( ! $this->isEmptyInlineGeometryNode($child) || $this->containsExcludedMedia($child) ) { - return null; - } - $layout = $layout || $this->emptyElementFootprint->declaresEmptyInlineLayoutBox($child); - $inner[] = $this->emptyInlineGeometryShell($child); - } - if ( ! $layout || array() === $inner ) { - return null; - } - - return $this->layoutShells->blockForElements(array( $element ), $inner, $element); - } - - private function emptyInlineGeometryShell(DOMElement $element): array - { - $wrappers = array( $element ); - $branch = $element; - while ( true ) { - $children = SourceDom::elementElementChildren($branch); - if ( 1 !== count($children) || ! $this->isEmptyInlineGeometryNode($children[0]) ) { - break; - } - $child = $children[0]; - $wrappers[] = $child; - $branch = $child; - if ( count(SourceDom::elementElementChildren($child)) > 1 ) { - break; - } - } - $inner = array(); - foreach ( SourceDom::elementElementChildren($branch) as $child ) { - if ( $child === (array() === $wrappers ? null : $wrappers[array_key_last($wrappers)]) || ! $this->isEmptyInlineGeometryNode($child) ) { - continue; - } - $inner[] = count(SourceDom::elementElementChildren($child)) > 1 - ? $this->emptyInlineGeometryShell($child) - : $this->layoutShells->blockForElements($this->emptyInlineWrapperChain($child), array(), $child); - } - - return $this->layoutShells->blockForElements($wrappers, $inner, $element); - } - - /** - * @return list - */ - private function emptyInlineWrapperChain(DOMElement $element): array - { - $chain = array( $element ); - $cursor = $element; - while ( true ) { - $children = SourceDom::elementElementChildren($cursor); - if ( 1 !== count($children) || ! $this->isEmptyInlineGeometryNode($children[0]) ) { - break; - } - $child = $children[0]; - if ( '' === trim($this->attr($child, 'class')) && '' === trim($this->attr($child, 'style')) && 0 === $child->childElementCount ) { - break; - } - $chain[] = $child; - $cursor = $child; - } - - return $chain; - } - - private function isEmptyInlineGeometryNode(DOMElement $element): bool - { - return $this->sourceElementClassifier->isInlineContentElement(strtolower($element->tagName)) - && ! $this->hasPaintedSourceText($element) - && ! $this->containsExcludedMedia($element) - && ! $this->runtimeIslands->isRuntimeDomTarget($element); - } - - private function hasPaintedSourceText(DOMElement $element): bool - { - if ( '' !== trim($element->textContent ?? '') ) { - return true; - } - foreach ( $element->getElementsByTagName('*') as $node ) { - if ( $node instanceof DOMElement && '' !== trim($this->attr($node, 'data-text')) ) { - return true; - } - } - - return false; - } - - private function containsExcludedMedia(DOMElement $element): bool - { - foreach ( array_merge(array( $element ), SourceDom::descendantElements($element)) as $node ) { - $tag = strtolower($node->tagName); - if ( in_array($tag, array( 'audio', 'canvas', 'iframe', 'img', 'object', 'picture', 'svg', 'video' ), true) ) { - 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