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
4 changes: 3 additions & 1 deletion php-transformer/src/HtmlToBlocks/BlockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions php-transformer/src/HtmlToBlocks/Support/SourceDom.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<img>` is an inline image that RichText
* carries as its `core/image` object format: a real, safe source sitting
Expand Down
83 changes: 83 additions & 0 deletions php-transformer/tests/unit/role-img-image-graphic-native-group.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);

require __DIR__ . '/../../vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer;

/**
* An element with `role="img"` whose content is only images is one named
* graphic, not phrasing content. Beside a sibling text span it used to be
* lowered into the parent's RichText, where its images failed the inline image
* gate and the whole parent became core/html. Inside a link tile the same
* RichText rejection fell through to fallback reduction, which turned the tile
* into a core/button whose label held the images.
*
* The graphic is now a core/group of core/image blocks carrying the accessible
* name through the Group's ariaLabel support; the sibling text still converts.
*/

$collect = static function (array $blocks, string $name) use (&$collect): array {
$found = array();
foreach ( $blocks as $block ) {
if ( $name === ($block['blockName'] ?? '') ) {
$found[] = $block;
}
array_push($found, ...$collect($block['innerBlocks'] ?? array(), $name));
}
return $found;
};

$failures = array();
$mosaic = '<span class="mosaic" role="img" aria-label="Plates"><img src="a.png" alt=""><img src="b.png" alt=""></span>';

foreach ( array(
'unnamed graphic beside text' => array( '<main><div><span role="img"><img src="a.png" alt=""></span><span>04</span></div></main>', 1, '' ),
'named single-image graphic' => array( '<main><div><span role="img" aria-label="Plates"><img src="a.png" alt=""></span><span>04</span></div></main>', 1, 'Plates' ),
'named mosaic beside text' => array( '<main><div class="card-image">' . $mosaic . '<span>04</span></div></main>', 2, 'Plates' ),
'named mosaic in a link tile' => array( '<main><a class="tile" href="/x/">' . $mosaic . '<span class="overlay"></span><span class="index">04</span><span class="copy"><strong>Title</strong><small>Sub</small></span></a></main>', 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, '<!-- wp:html') ) {
$failures[] = $name . ': emitted core/html: ' . $markup;
}
if ( array() !== $collect($blocks, 'core/button') ) {
$failures[] = $name . ': emitted a core/button: ' . $markup;
}
if ( $imageCount !== count($collect($blocks, 'core/image')) ) {
$failures[] = $name . ': expected ' . $imageCount . ' core/image blocks: ' . $markup;
}

$graphic = null;
foreach ( $collect($blocks, 'core/group') as $group ) {
$children = $group['innerBlocks'] ?? array();
if ( array() !== $children && array() === array_filter($children, static fn (array $child): bool => 'core/image' !== ($child['blockName'] ?? '')) ) {
$graphic = $group;
}
}
if ( null === $graphic || $imageCount !== count($graphic['innerBlocks']) ) {
$failures[] = $name . ': expected one core/group holding the graphic images: ' . $markup;
} elseif ( $label !== (string) ($graphic['attrs']['ariaLabel'] ?? '') ) {
$failures[] = $name . ': expected the group ariaLabel "' . $label . '": ' . $markup;
} elseif ( '' !== $label && 1 !== preg_match('#<div class="wp-block-group[^"]*" aria-label="' . preg_quote($label, '#') . '">#', $markup) ) {
$failures[] = $name . ': expected the saved group to carry aria-label: ' . $markup;
}

$texts = implode(' ', array_map(static fn (array $paragraph): string => strip_tags((string) ($paragraph['innerHTML'] ?? '')), $collect($blocks, 'core/paragraph')));
if ( ! str_contains($texts, '04') ) {
$failures[] = $name . ': lost the sibling "04" text: ' . $markup;
}
if ( 'pass' !== ($result['source_reports']['wp_block_validity']['status'] ?? '') ) {
$failures[] = $name . ': block validity did not pass: ' . json_encode($result['source_reports']['wp_block_validity'] ?? null);
}
}

if ( array() !== $failures ) {
fwrite(STDERR, implode("\n", $failures) . "\n");
exit(1);
}

echo "role-img-image-graphic-native-group: ok\n";
Loading