diff --git a/php-transformer/src/HtmlToBlocks/Elements/CustomWrapperConventions.php b/php-transformer/src/HtmlToBlocks/Elements/CustomWrapperConventions.php
new file mode 100644
index 000000000..6db5d658b
--- /dev/null
+++ b/php-transformer/src/HtmlToBlocks/Elements/CustomWrapperConventions.php
@@ -0,0 +1,163 @@
+
+ */
+ public function authorDataAttributeSelectors(string $css): array
+ {
+ $selectors = array();
+ foreach ( CssValueSplitter::ruleBlocks($css) as $rule ) {
+ if ( ! str_contains($rule['selector'], '[data-') ) {
+ continue;
+ }
+ if ( preg_match_all('/\[\s*data-[A-Za-z][A-Za-z0-9_-]*\s*(?:[*^$|~]?=\s*(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\]\s"\']+)\s*(?:[is]\s*)?)?\]/', $rule['selector'], $matches) ) {
+ foreach ( $matches[0] as $selector ) {
+ $selectors[$selector] = true;
+ }
+ }
+ }
+
+ return $selectors;
+ }
+
+ /**
+ * Whether an author selector keys on this instance through its own
+ * attributes. Such an instance is a selector hook, not a wrapping
+ * convention: removing it silently detaches every rule that needs it as
+ * a subject or ancestor. Equality compounds must match the instance's
+ * value; any other operator fails closed on the attribute name.
+ *
+ * @param array $selectors
+ */
+ public function authorAttributeSelectorAddresses(DOMElement $element, array $selectors): bool
+ {
+ foreach ( array_keys($selectors) as $selector ) {
+ $parsed = \Automattic\BlocksEngine\PhpTransformer\Support\RuntimeSelectorVocabulary::parseAttributeSelector($selector);
+ if ( null !== $parsed ) {
+ if ( \Automattic\BlocksEngine\PhpTransformer\Support\RuntimeSelectorVocabulary::matchesElement($element, $selector, array()) ) {
+ return true;
+ }
+ continue;
+ }
+ if ( 1 === preg_match('/^\[\s*(data-[A-Za-z][A-Za-z0-9_-]*)/', $selector, $name) && $element->hasAttribute(strtolower($name[1])) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether a custom element tag is used as a wrapping convention rather than
+ * as a component host.
+ *
+ * @param array $elements Every instance of the tag.
+ */
+ public function isWrapperConventionTag(string $tag, array $elements, string $staticCss): bool
+ {
+ if ( self::WRAPPER_CONVENTION_MIN_INSTANCES > count($elements) ) {
+ return false;
+ }
+
+ // A tag that carries presentation of its own is not render-neutral.
+ // `display:contents` is the exception that proves the rule: it states
+ // the element generates no box and renders as its children.
+ if ( ! $this->customElementCssIsRenderNeutral($tag, $staticCss) ) {
+ return false;
+ }
+
+ $shapes = array();
+ foreach ( $elements as $element ) {
+ $shapes[$this->wrappedContentShape($element)] = true;
+ if ( 1 < count($shapes) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether every author rule addressing a custom element tag leaves it
+ * render-neutral, so replacing instances with their children preserves the
+ * authored presentation.
+ */
+ private function customElementCssIsRenderNeutral(string $tag, string $css): bool
+ {
+ if ( '' === trim($css) ) {
+ return true;
+ }
+
+ $pattern = '/(?styleResolver->cssDeclarations($rule['declarations']) as $property => $value ) {
+ if ( 'display' !== strtolower(trim($property)) || 'contents' !== strtolower(trim($value)) ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Shallow tag skeleton of the content a wrapper carries. Instances of a real
+ * component repeat this shape; instances of a wrapper do not.
+ */
+ private function wrappedContentShape(DOMElement $element, int $depth = 0): string
+ {
+ $parts = array();
+ foreach ( $element->childNodes as $child ) {
+ if ( ! $child instanceof DOMElement ) {
+ continue;
+ }
+ $tag = strtolower($child->tagName);
+ $parts[] = self::WRAPPER_CONTENT_SHAPE_DEPTH > $depth
+ ? $tag . '(' . $this->wrappedContentShape($child, $depth + 1) . ')'
+ : $tag;
+ }
+
+ return implode(',', $parts);
+ }
+}
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/Elements/EmptyVisualSpacerLowering.php b/php-transformer/src/HtmlToBlocks/Elements/EmptyVisualSpacerLowering.php
new file mode 100644
index 000000000..58c6df0e1
--- /dev/null
+++ b/php-transformer/src/HtmlToBlocks/Elements/EmptyVisualSpacerLowering.php
@@ -0,0 +1,149 @@
+ */
+ private function emptyVisualElementAttributes(DOMElement $element): array
+ {
+ $attrs = $this->styleResolver->presentationAttributes($element);
+ $parent = $element->parentNode;
+ if ( ! $parent instanceof DOMElement ) {
+ return $attrs;
+ }
+
+ $parentDeclarations = $this->styleResolver->structuralPresentationDeclarations($parent);
+ $parentDisplay = CssValueInspector::comparable((string) ($parentDeclarations['display'] ?? ''));
+ if ( ! in_array($parentDisplay, array( 'flex', 'inline-flex' ), true) ) {
+ return $attrs;
+ }
+
+ // Resolve the element's full matched CSS, not only its inline style:
+ // a utility-class divider (e.g. Tailwind's `h-px w-24`) declares its
+ // geometry on a stylesheet rule that the narrower inline-only
+ // resolver used here previously never saw, so an authored box was
+ // misread as sizeless.
+ $declarations = $this->styleResolver->structuralPresentationDeclarations($element);
+ $position = strtolower(trim((string) ($declarations['position'] ?? 'static')));
+ if ( in_array($position, array( 'absolute', 'fixed' ), true) ) {
+ return $attrs;
+ }
+
+ // The zero-collapse compatibility box below only removes the flex
+ // main-axis footprint core would otherwise add to a childless flex
+ // item. Which CSS properties own that axis depends on the parent's
+ // flex-direction: a row container's main axis is width, a column
+ // container's is height. Checking width alone let an authored
+ // height survive the escape hatch but still be crushed by the
+ // row-shaped compatibility box below.
+ $isColumnAxis = in_array(strtolower(trim((string) ($parentDeclarations['flex-direction'] ?? 'row'))), array( 'column', 'column-reverse' ), true);
+ $mainAxisSizeProperties = $isColumnAxis
+ ? array( 'height', 'min-height', 'max-height', 'flex', 'flex-basis' )
+ : array( 'width', 'min-width', 'max-width', 'flex', 'flex-basis' );
+ foreach ( $mainAxisSizeProperties as $property ) {
+ if ( isset($declarations[$property]) && '' !== trim($declarations[$property]) && 'auto' !== strtolower(trim($declarations[$property])) ) {
+ return $attrs;
+ }
+ }
+ foreach ( $this->session->sourceStyleResolutionState()->pseudoElementRules() as $rule ) {
+ if ( $this->styleResolver->matchesCssSelector($element, $rule['selector']) && array_intersect_key($rule['declarations'], array_flip(array( 'content', 'display', 'width', 'min-width' ))) ) {
+ return $attrs;
+ }
+ }
+
+ $attrs['className'] = trim((string) ($attrs['className'] ?? '') . ' ' . ( $isColumnAxis ? self::EMPTY_FLEX_ITEM_COLUMN_CLASS : self::EMPTY_FLEX_ITEM_CLASS ));
+ if ( $this->runtimeIslands->isRuntimeDomTarget($element) ) {
+ $attrs['className'] = trim($attrs['className'] . ' ' . HtmlTransformer::EMPTY_RUNTIME_TARGET_CLASS);
+ $this->session->runtimeBehaviorState()->markEmptyRuntimeTargetGenerated();
+ }
+ return $attrs;
+ }
+
+ /** @return array */
+ public function emptyVisualSpacerBlock(DOMElement $element): array
+ {
+ $attrs = $this->emptyVisualElementAttributes($element);
+ if ( ! $this->emptyElementFootprint->isEmptyVisualInlineCandidate($element) ) {
+ $attrs['className'] = SourceDom::mergeClassNames((string) ($attrs['className'] ?? ''), HtmlTransformer::EMPTY_VISUAL_GROUP_CLASS);
+ $this->session->runtimeBehaviorState()->markEmptyVisualGroupGenerated();
+ // Carry complex paint, but let native background support own a
+ // CSS block box's solid colour so editor recolouring can win.
+ $attrs['className'] = SourceDom::mergeClassNames(
+ (string) $attrs['className'],
+ $this->styleResolver->emptyElementBackgroundCarrierClassName($element, $this->emptyElementFootprint->hasEmptyVisualBlockDisplay($element) && isset($attrs['style']['color']['background']))
+ );
+ // Empty, nested layout wrappers can still own measurable grid or
+ // flex tracks (fluid-engine sections commonly put those tracks on
+ // a child below a data-bearing presentation wrapper). Lowering the
+ // outer visual carrier as an empty group used to discard that
+ // child and its selector identity, collapsing its authored tracks.
+ $innerBlocks = array();
+ if ( 0 < SourceDom::childElementCount($element)
+ && '' === SourceDom::renderedTextContent($element)
+ && $this->hasEmptyGridTrackDescendant($element)
+ ) {
+ $ignoredFallbacks = array();
+ $innerBlocks = $this->tree->children($element, $ignoredFallbacks, true);
+ }
+ $block = $this->blocks->createBlock('core/group', $attrs, $innerBlocks, $element);
+ $block['_editability_visual_owned'] = true;
+ return $block;
+ }
+
+ $declarations = $this->styleResolver->structuralPresentationDeclarations($element);
+ $attrs['height'] = $this->styleResolver->resolveCssVariablesInValue($declarations['height']);
+ $attrs['width'] = $this->styleResolver->resolveCssVariablesInValue($declarations['width']);
+
+ return $this->blocks->createBlock('core/spacer', $attrs, array(), $element);
+ }
+
+ private function hasEmptyGridTrackDescendant(DOMElement $element): bool
+ {
+ foreach ( $element->getElementsByTagName('*') as $descendant ) {
+ if ( $descendant instanceof DOMElement
+ && '' === SourceDom::renderedTextContent($descendant)
+ && $this->emptyElementFootprint->hasAuthoredGridTracks($descendant)
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
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..f8e7a3042 100644
--- a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php
+++ b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php
@@ -209,16 +209,6 @@ final class HtmlCompilation extends SourceBlockCreator implements RichTextInline
{
private const GENERATED_COMPONENT_MIN_SOURCE_DEPTH = 14;
- /**
- * Instances a custom element tag must reach before recurring usage can be
- * read as a wrapping convention rather than a component.
- */
- private const WRAPPER_CONVENTION_MIN_INSTANCES = 3;
-
- /**
- * Depth of the shallow tag skeleton compared across wrapper instances.
- */
- private const WRAPPER_CONTENT_SHAPE_DEPTH = 2;
use DomHelpersTrait;
private readonly AuthorLayoutOwnership $authorLayoutOwnership;
@@ -314,6 +304,10 @@ 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 Elements\EmptyInlineGeometryLowering $emptyInlineGeometry;
+ private readonly Elements\CustomWrapperConventions $wrapperConventions;
+ private readonly Elements\EmptyVisualSpacerLowering $emptyVisualSpacers;
private readonly SourceProvenance $sourceProvenance;
private readonly RuntimeResourceElementConverter $runtimeResourceConverter;
@@ -430,17 +424,6 @@ final class HtmlCompilation extends SourceBlockCreator implements RichTextInline
private const INLINE_LAYOUT_CARRIER_CLASS = AuthorStylesheetProjector::INLINE_LAYOUT_CARRIER_CLASS;
- private const EMPTY_FLEX_ITEM_CLASS = 'blocks-engine-empty-flex-item';
-
- /**
- * Marks a childless flex item inside a column-direction (or
- * column-reverse) container. The main axis there is height, not width,
- * so the compatibility box this class carries zeroes the opposite
- * properties from {@see EMPTY_FLEX_ITEM_CLASS}'s row-axis assumption.
- */
- private const EMPTY_FLEX_ITEM_COLUMN_CLASS = 'blocks-engine-empty-flex-column-item';
-
-
public const EMPTY_VISUAL_GROUP_CLASS = 'blocks-engine-empty-visual-group';
/**
* Marks a core/paragraph lowered from a margin-styled text-only div. The
@@ -605,6 +588,10 @@ 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->emptyInlineGeometry = new Elements\EmptyInlineGeometryLowering($this, $this->session, $this->sourceElementClassifier, $this->emptyElementFootprint, $this->authoredStateButtons, $this->layoutShells, $this->runtimeIslands);
+ $this->wrapperConventions = new Elements\CustomWrapperConventions($this->styleResolver);
+ $this->emptyVisualSpacers = new Elements\EmptyVisualSpacerLowering($this, $this, $this->session, $this->styleResolver, $this->emptyElementFootprint, $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 +632,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),
@@ -684,7 +671,7 @@ function (DOMElement $element): bool {
fn (DOMElement $element, string $tagName): ?DOMElement => SourceDom::ancestorElement($element, $tagName),
fn (DOMElement $element): bool => $this->listStructure->isStructuralListItem($element),
fn (DOMElement $element): bool => $this->shouldPreserveEmptyVisualElement($element),
- fn (DOMElement $element): array => $this->emptyVisualSpacerBlock($element),
+ fn (DOMElement $element): array => $this->emptyVisualSpacers->emptyVisualSpacerBlock($element),
function (DOMElement $element): void { $this->rememberNativeDisclosure($element); },
fn (DOMElement $element): bool => $this->runtimeIslands->retainsRuntimeTextLeafInRichText($element)
), $this->styleResolver, $this->runtime, $this->sourceBlockAttributeProjector);
@@ -954,7 +941,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
);
@@ -1057,7 +1044,7 @@ function (DOMElement $element, array &$fallbacks, bool $captureUnsupported): arr
backgroundImageBlock: fn (DOMElement $element): ?array => $this->imageLowering->backgroundImageBlock($element),
coalescedSingleGroupWrapper: fn (DOMElement $element, array $child): ?array => $this->coalescedSingleGroupWrapper($element, $child),
shouldPreserveWrapper: fn (DOMElement $element, ?string $asTag = null): bool => $this->shouldPreserveWrapper($element, $asTag),
- emptyVisualSpacerBlock: fn (DOMElement $element): array => $this->emptyVisualSpacerBlock($element)
+ emptyVisualSpacerBlock: fn (DOMElement $element): array => $this->emptyVisualSpacers->emptyVisualSpacerBlock($element)
));
$this->mediaDispatchConverter = new MediaDispatchElementConverter(new MediaDispatchElementContext(
function (DOMElement $element, array &$fallbacks): ?array {
@@ -1094,7 +1081,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,
@@ -1334,7 +1321,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 ));
@@ -2259,13 +2246,13 @@ private function unwrapRenderNeutralCustomElements(DOMElement $body, string $sta
$attributeSelectors = null;
foreach ( $instances as $tag => $elements ) {
- if ( ! $this->isWrapperConventionTag($tag, $elements, $staticCss) ) {
+ if ( ! $this->wrapperConventions->isWrapperConventionTag($tag, $elements, $staticCss) ) {
continue;
}
- $attributeSelectors ??= $this->authorDataAttributeSelectors($staticCss);
+ $attributeSelectors ??= $this->wrapperConventions->authorDataAttributeSelectors($staticCss);
foreach ( $elements as $element ) {
if ( $this->renderNeutral->isRenderNeutralWrapper($element, false) ) {
- if ( $this->authorAttributeSelectorAddresses($element, $attributeSelectors) ) {
+ if ( $this->wrapperConventions->authorAttributeSelectorAddresses($element, $attributeSelectors) ) {
$this->retainedCustomWrapperHooks[] = $element;
} else {
$this->richTextStylingHooks()->unwrapElement($element);
@@ -2275,86 +2262,6 @@ private function unwrapRenderNeutralCustomElements(DOMElement $body, string $sta
}
}
- /**
- * Data-attribute compounds written in author selectors, e.g. the
- * `[data-key="a"]` of `[data-key="a"] .photo`.
- *
- * @return array
- */
- private function authorDataAttributeSelectors(string $css): array
- {
- $selectors = array();
- foreach ( CssValueSplitter::ruleBlocks($css) as $rule ) {
- if ( ! str_contains($rule['selector'], '[data-') ) {
- continue;
- }
- if ( preg_match_all('/\[\s*data-[A-Za-z][A-Za-z0-9_-]*\s*(?:[*^$|~]?=\s*(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\]\s"\']+)\s*(?:[is]\s*)?)?\]/', $rule['selector'], $matches) ) {
- foreach ( $matches[0] as $selector ) {
- $selectors[$selector] = true;
- }
- }
- }
-
- return $selectors;
- }
-
- /**
- * Whether an author selector keys on this instance through its own
- * attributes. Such an instance is a selector hook, not a wrapping
- * convention: removing it silently detaches every rule that needs it as
- * a subject or ancestor. Equality compounds must match the instance's
- * value; any other operator fails closed on the attribute name.
- *
- * @param array $selectors
- */
- private function authorAttributeSelectorAddresses(DOMElement $element, array $selectors): bool
- {
- foreach ( array_keys($selectors) as $selector ) {
- $parsed = \Automattic\BlocksEngine\PhpTransformer\Support\RuntimeSelectorVocabulary::parseAttributeSelector($selector);
- if ( null !== $parsed ) {
- if ( \Automattic\BlocksEngine\PhpTransformer\Support\RuntimeSelectorVocabulary::matchesElement($element, $selector, array()) ) {
- return true;
- }
- continue;
- }
- if ( 1 === preg_match('/^\[\s*(data-[A-Za-z][A-Za-z0-9_-]*)/', $selector, $name) && $element->hasAttribute(strtolower($name[1])) ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Whether a custom element tag is used as a wrapping convention rather than
- * as a component host.
- *
- * @param array $elements Every instance of the tag.
- */
- private function isWrapperConventionTag(string $tag, array $elements, string $staticCss): bool
- {
- if ( self::WRAPPER_CONVENTION_MIN_INSTANCES > count($elements) ) {
- return false;
- }
-
- // A tag that carries presentation of its own is not render-neutral.
- // `display:contents` is the exception that proves the rule: it states
- // the element generates no box and renders as its children.
- if ( ! $this->customElementCssIsRenderNeutral($tag, $staticCss) ) {
- return false;
- }
-
- $shapes = array();
- foreach ( $elements as $element ) {
- $shapes[$this->wrappedContentShape($element)] = true;
- if ( 1 < count($shapes) ) {
- return true;
- }
- }
-
- return false;
- }
-
/**
* Author CSS visible to the document: declared stylesheets plus the styles
* the source embeds inline.
@@ -2364,56 +2271,6 @@ private function authoredCssText(string $html, string $staticCss): string
return $this->stylesheetAnalysisComposer->combinedAuthorStylesheet($html, $staticCss);
}
- /**
- * Whether every author rule addressing a custom element tag leaves it
- * render-neutral, so replacing instances with their children preserves the
- * authored presentation.
- */
- private function customElementCssIsRenderNeutral(string $tag, string $css): bool
- {
- if ( '' === trim($css) ) {
- return true;
- }
-
- $pattern = '/(?styleResolver->cssDeclarations($rule['declarations']) as $property => $value ) {
- if ( 'display' !== strtolower(trim($property)) || 'contents' !== strtolower(trim($value)) ) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Shallow tag skeleton of the content a wrapper carries. Instances of a real
- * component repeat this shape; instances of a wrapper do not.
- */
- private function wrappedContentShape(DOMElement $element, int $depth = 0): string
- {
- $parts = array();
- foreach ( $element->childNodes as $child ) {
- if ( ! $child instanceof DOMElement ) {
- continue;
- }
- $tag = strtolower($child->tagName);
- $parts[] = self::WRAPPER_CONTENT_SHAPE_DEPTH > $depth
- ? $tag . '(' . $this->wrappedContentShape($child, $depth + 1) . ')'
- : $tag;
- }
-
- return implode(',', $parts);
- }
-
private function collectGeneratedComponentCandidates(DOMElement $element, int $depth = 0): void
{
if (self::GENERATED_COMPONENT_MIN_SOURCE_DEPTH <= $depth
@@ -3471,7 +3328,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
// unsupported-element fallback.
if ( SourceDom::isEmptyNonNavigatingAnchor($element) ) {
return $this->shouldPreserveEmptyVisualElement($element)
- ? $this->emptyVisualSpacerBlock($element)
+ ? $this->emptyVisualSpacers->emptyVisualSpacerBlock($element)
: null;
}
@@ -3566,7 +3423,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 +3656,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
*/
@@ -4947,113 +4766,6 @@ private function isExplicitlyDisplayNoneEmptyElement(DOMElement $element): bool
&& array() === SourceDom::safeDataAttributes($element);
}
- /** @return array */
- private function emptyVisualElementAttributes(DOMElement $element): array
- {
- $attrs = $this->styleResolver->presentationAttributes($element);
- $parent = $element->parentNode;
- if ( ! $parent instanceof DOMElement ) {
- return $attrs;
- }
-
- $parentDeclarations = $this->styleResolver->structuralPresentationDeclarations($parent);
- $parentDisplay = CssValueInspector::comparable((string) ($parentDeclarations['display'] ?? ''));
- if ( ! in_array($parentDisplay, array( 'flex', 'inline-flex' ), true) ) {
- return $attrs;
- }
-
- // Resolve the element's full matched CSS, not only its inline style:
- // a utility-class divider (e.g. Tailwind's `h-px w-24`) declares its
- // geometry on a stylesheet rule that the narrower inline-only
- // resolver used here previously never saw, so an authored box was
- // misread as sizeless.
- $declarations = $this->styleResolver->structuralPresentationDeclarations($element);
- $position = strtolower(trim((string) ($declarations['position'] ?? 'static')));
- if ( in_array($position, array( 'absolute', 'fixed' ), true) ) {
- return $attrs;
- }
-
- // The zero-collapse compatibility box below only removes the flex
- // main-axis footprint core would otherwise add to a childless flex
- // item. Which CSS properties own that axis depends on the parent's
- // flex-direction: a row container's main axis is width, a column
- // container's is height. Checking width alone let an authored
- // height survive the escape hatch but still be crushed by the
- // row-shaped compatibility box below.
- $isColumnAxis = in_array(strtolower(trim((string) ($parentDeclarations['flex-direction'] ?? 'row'))), array( 'column', 'column-reverse' ), true);
- $mainAxisSizeProperties = $isColumnAxis
- ? array( 'height', 'min-height', 'max-height', 'flex', 'flex-basis' )
- : array( 'width', 'min-width', 'max-width', 'flex', 'flex-basis' );
- foreach ( $mainAxisSizeProperties as $property ) {
- if ( isset($declarations[$property]) && '' !== trim($declarations[$property]) && 'auto' !== strtolower(trim($declarations[$property])) ) {
- return $attrs;
- }
- }
- foreach ( $this->session->sourceStyleResolutionState()->pseudoElementRules() as $rule ) {
- if ( $this->styleResolver->matchesCssSelector($element, $rule['selector']) && array_intersect_key($rule['declarations'], array_flip(array( 'content', 'display', 'width', 'min-width' ))) ) {
- return $attrs;
- }
- }
-
- $attrs['className'] = trim((string) ($attrs['className'] ?? '') . ' ' . ( $isColumnAxis ? self::EMPTY_FLEX_ITEM_COLUMN_CLASS : self::EMPTY_FLEX_ITEM_CLASS ));
- if ( $this->runtimeIslands->isRuntimeDomTarget($element) ) {
- $attrs['className'] = trim($attrs['className'] . ' ' . self::EMPTY_RUNTIME_TARGET_CLASS);
- $this->session->runtimeBehaviorState()->markEmptyRuntimeTargetGenerated();
- }
- return $attrs;
- }
-
- /** @return array */
- private function emptyVisualSpacerBlock(DOMElement $element): array
- {
- $attrs = $this->emptyVisualElementAttributes($element);
- if ( ! $this->emptyElementFootprint->isEmptyVisualInlineCandidate($element) ) {
- $attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), self::EMPTY_VISUAL_GROUP_CLASS);
- $this->session->runtimeBehaviorState()->markEmptyVisualGroupGenerated();
- // Carry complex paint, but let native background support own a
- // CSS block box's solid colour so editor recolouring can win.
- $attrs['className'] = $this->mergeClassNames(
- (string) $attrs['className'],
- $this->styleResolver->emptyElementBackgroundCarrierClassName($element, $this->emptyElementFootprint->hasEmptyVisualBlockDisplay($element) && isset($attrs['style']['color']['background']))
- );
- // Empty, nested layout wrappers can still own measurable grid or
- // flex tracks (fluid-engine sections commonly put those tracks on
- // a child below a data-bearing presentation wrapper). Lowering the
- // outer visual carrier as an empty group used to discard that
- // child and its selector identity, collapsing its authored tracks.
- $innerBlocks = array();
- if ( 0 < $this->childElementCount($element)
- && '' === SourceDom::renderedTextContent($element)
- && $this->hasEmptyGridTrackDescendant($element)
- ) {
- $ignoredFallbacks = array();
- $innerBlocks = $this->convertChildren($element, $ignoredFallbacks, true);
- }
- $block = $this->createBlock('core/group', $attrs, $innerBlocks, $element);
- $block['_editability_visual_owned'] = true;
- return $block;
- }
-
- $declarations = $this->styleResolver->structuralPresentationDeclarations($element);
- $attrs['height'] = $this->styleResolver->resolveCssVariablesInValue($declarations['height']);
- $attrs['width'] = $this->styleResolver->resolveCssVariablesInValue($declarations['width']);
-
- return $this->createBlock('core/spacer', $attrs, array(), $element);
- }
-
- private function hasEmptyGridTrackDescendant(DOMElement $element): bool
- {
- foreach ( $element->getElementsByTagName('*') as $descendant ) {
- if ( $descendant instanceof DOMElement
- && '' === SourceDom::renderedTextContent($descendant)
- && $this->emptyElementFootprint->hasAuthoredGridTracks($descendant)
- ) {
- return true;
- }
- }
- return false;
- }
-
/**
* An empty href-less named `` is a fragment target. Preserve its
* identifier on a native block HTML anchor rather than recording it as
@@ -5077,7 +4789,7 @@ private function emptyNamedFragmentTargetBlock(DOMElement $element): ?array
$element->setAttribute('id', $id);
}
- return $this->emptyVisualSpacerBlock($element);
+ return $this->emptyVisualSpacers->emptyVisualSpacerBlock($element);
}
private function hasEmptyVisualInlineChild(DOMElement $element): bool
@@ -6696,12 +6408,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.
@@ -6764,182 +6470,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;
- }
-
- 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