Skip to content
Merged
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
123 changes: 123 additions & 0 deletions php-transformer/src/HtmlToBlocks/Elements/InertElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Elements;

use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Classification\SourceElementClassifier;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\SourceElementVisibility;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\StyleResolver;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Support\SourceDom;
use DOMDocument;
use DOMElement;

/**
* Recognizes source elements that are inert on the page: runtime media
* placeholders that never paint, and hidden SVG sprite stores that only
* other SVG content consumes.
*/
final class InertElements
{
public function __construct(
private readonly StyleResolver $styleResolver,
private readonly SourceElementClassifier $sourceElementClassifier,
private readonly SourceElementVisibility $sourceVisibility,
private readonly RuntimeIslandAnalyzer $runtimeIslands
) {
}

public 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(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;
}
}
105 changes: 6 additions & 99 deletions php-transformer/src/HtmlToBlocks/HtmlCompilation.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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;
Expand Down Expand Up @@ -607,6 +608,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(
Expand Down Expand Up @@ -647,7 +649,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),
Expand Down Expand Up @@ -956,7 +958,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
);
Expand Down Expand Up @@ -1096,7 +1098,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,
Expand Down Expand Up @@ -3568,7 +3570,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
return $transparentCustomElement;
}

if ( $this->isInertRuntimeMediaPlaceholder($element) ) {
if ( $this->inertElements->isInertRuntimeMediaPlaceholder($element) ) {
return null;
}

Expand Down Expand Up @@ -3801,44 +3803,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<string, mixed>|null
*/
Expand Down Expand Up @@ -6798,63 +6762,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
Expand Down
Loading