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 506481c7f..25f009824 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php +++ b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php @@ -316,6 +316,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; @@ -609,6 +610,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( @@ -1338,7 +1340,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 )); @@ -6575,12 +6577,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. @@ -6643,125 +6639,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