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' => '' . $tag . '>' );
+ // 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' => '' . $tag . '>' );
}
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, '