diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index 14bda0102..04c5f5a34 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -517,7 +517,9 @@ private function blockHtml(string $name, array $attrs, array $innerBlocks): stri if ( 'core/group' === $name ) { $tag = $this->groupTagName($attrs['tagName'] ?? null); - return array( 'opening' => '<' . $tag . $this->blockSupportAttrs($attrs, 'wp-block-group') . '>', 'closing' => '' ); + // Core's ariaLabel block support saves the attribute as aria-label. + $ariaLabel = $this->htmlAttrs(array( 'aria-label' => (string) ($attrs['ariaLabel'] ?? '') )); + return array( 'opening' => '<' . $tag . $this->blockSupportAttrs($attrs, 'wp-block-group') . $ariaLabel . '>', 'closing' => '' ); } return ''; diff --git a/php-transformer/src/HtmlToBlocks/Classification/SourceElementClassifier.php b/php-transformer/src/HtmlToBlocks/Classification/SourceElementClassifier.php index 3c8ea080c..b3e3d7450 100644 --- a/php-transformer/src/HtmlToBlocks/Classification/SourceElementClassifier.php +++ b/php-transformer/src/HtmlToBlocks/Classification/SourceElementClassifier.php @@ -422,7 +422,7 @@ public function hasOnlyPhrasingChildren(DOMElement $element): bool continue; } - if ( 'br' === $tagName || $this->isInlineContentElement($tagName) ) { + if ( 'br' === $tagName || ( $this->isInlineContentElement($tagName) && ! SourceDom::isImageOnlyNamedGraphic($child) ) ) { $nonAnchorText = true; continue; } diff --git a/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php b/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php index 7e6a072e5..6d29802ee 100644 --- a/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php +++ b/php-transformer/src/HtmlToBlocks/Elements/ButtonLinkDispatcher.php @@ -174,7 +174,8 @@ private function hasLinkWrapperChildren(DOMElement $element): bool } $tagName = strtolower($child->tagName); - if ( in_array($tagName, array( 'br', 'img', 'svg' ), true) || $this->sourceElementClassifier->isInlineContentElement($tagName) ) { + if ( in_array($tagName, array( 'br', 'img', 'svg' ), true) + || ( $this->sourceElementClassifier->isInlineContentElement($tagName) && ! SourceDom::isImageOnlyNamedGraphic($child) ) ) { continue; } diff --git a/php-transformer/src/HtmlToBlocks/Elements/InlineContentElementConverter.php b/php-transformer/src/HtmlToBlocks/Elements/InlineContentElementConverter.php index 26096288a..ec50d6480 100644 --- a/php-transformer/src/HtmlToBlocks/Elements/InlineContentElementConverter.php +++ b/php-transformer/src/HtmlToBlocks/Elements/InlineContentElementConverter.php @@ -70,6 +70,17 @@ public function convert(DOMElement $element, string $tagName, array &$fallbacks) } } + // A named graphic of images is one group of native images; the group + // carries the graphic's accessible name. + if ( SourceDom::isImageOnlyNamedGraphic($element) ) { + $attrs = $this->styleResolver->presentationAttributes($element); + $label = trim(SourceDom::attr($element, 'aria-label')); + if ( '' !== $label ) { + $attrs['ariaLabel'] = $label; + } + return ConversionOutcome::handled($this->createBlock->createBlock('core/group', $attrs, $this->context->convertChildren($element, $fallbacks), $element)); + } + // Captured markup can place flow content inside an inline wrapper. A // native group retains that wrapper's presentation while its children // follow their own semantic conversion paths. diff --git a/php-transformer/src/HtmlToBlocks/Support/SourceDom.php b/php-transformer/src/HtmlToBlocks/Support/SourceDom.php index 96d04025f..bc579ffd9 100644 --- a/php-transformer/src/HtmlToBlocks/Support/SourceDom.php +++ b/php-transformer/src/HtmlToBlocks/Support/SourceDom.php @@ -918,6 +918,29 @@ public static function isInlineImageMark(DOMElement $element): bool && self::hasOnlySafeFallbackAttributes($element); } + /** + * An element with `role="img"` whose content is only images is one named + * graphic (ARIA makes its descendants presentational), not phrasing + * content: it becomes a group of native images carrying the accessible + * name, never inline RichText. + */ + public static function isImageOnlyNamedGraphic(DOMElement $element): bool + { + if ( 'img' !== strtolower(trim(self::attr($element, 'role'))) || 'img' === strtolower($element->tagName) || '' !== trim($element->textContent ?? '') ) { + return false; + } + + $images = 0; + foreach ( $element->getElementsByTagName('*') as $descendant ) { + if ( 'img' !== strtolower($descendant->tagName) ) { + return false; + } + ++$images; + } + + return 0 < $images; + } + /** * Whether a text element's every `` is an inline image that RichText * carries as its `core/image` object format: a real, safe source sitting diff --git a/php-transformer/tests/unit/role-img-image-graphic-native-group.php b/php-transformer/tests/unit/role-img-image-graphic-native-group.php new file mode 100644 index 000000000..42b11ef83 --- /dev/null +++ b/php-transformer/tests/unit/role-img-image-graphic-native-group.php @@ -0,0 +1,83 @@ +'; + +foreach ( array( + 'unnamed graphic beside text' => array( '
04
', 1, '' ), + 'named single-image graphic' => array( '
04
', 1, 'Plates' ), + 'named mosaic beside text' => array( '
' . $mosaic . '04
', 2, 'Plates' ), + 'named mosaic in a link tile' => array( '
' . $mosaic . '04TitleSub
', 2, 'Plates' ), +) as $name => [ $source, $imageCount, $label ] ) { + $result = ( new HtmlTransformer() )->transform($source)->toArray(); + $markup = (string) ($result['serialized_blocks'] ?? ''); + $blocks = $result['blocks'] ?? array(); + + if ( str_contains($markup, '