Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Elements;

use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Classification\FormControlClassifier;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Classification\SourceElementClassifier;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Session\HtmlTransformerSession;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\SourceBlockCreator;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\EmptyElementFootprint;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Support\SourceDom;
use DOMElement;

/**
* Lowers an empty inline tree whose source CSS still generates a line box or
* flex track: layout, not text and not a raw HTML island, kept in the
* existing layout shell.
*/
final class EmptyInlineGeometryLowering
{
public function __construct(
private readonly SourceBlockCreator $blocks,
private readonly HtmlTransformerSession $session,
private readonly SourceElementClassifier $sourceElementClassifier,
private readonly EmptyElementFootprint $emptyElementFootprint,
private readonly AuthoredStateButtonLowering $authoredStateButtons,
private readonly LayoutShellLowering $layoutShells,
private readonly RuntimeIslandAnalyzer $runtimeIslands
) {
}

/** @return array<string, mixed> */
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<int, array<string, mixed>> $fallbacks
* @return array<string, mixed>|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<DOMElement>
*/
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;
}
}
129 changes: 3 additions & 126 deletions php-transformer/src/HtmlToBlocks/HtmlCompilation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 ));
Expand Down Expand Up @@ -6575,12 +6577,6 @@ private function isDeclaredRuntimeDomTarget(DOMElement $element): bool
return false;
}

/** @return array<string, mixed> */
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.
Expand Down Expand Up @@ -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<int, array<string, mixed>> $fallbacks
* @return array<string, mixed>|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<DOMElement>
*/
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
Expand Down
Loading