diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
index a1139bb..34fcfbd 100644
--- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
+++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
@@ -391,6 +391,10 @@ private function collectSourceLandmarks(DOMElement $element, array &$counts, arr
private function landmarkKindForElement(DOMElement $element): string
{
$tagName = strtolower($element->tagName);
+ if ( 'footer' === $tagName && $this->hasAncestorTag($element, array( 'blockquote', 'figure' )) ) {
+ return '';
+ }
+
if ( in_array($tagName, array( 'header', 'nav', 'main', 'footer' ), true) ) {
return 'nav' === $tagName ? 'nav' : $tagName;
}
@@ -404,6 +408,20 @@ private function landmarkKindForElement(DOMElement $element): string
};
}
+ /**
+ * @param array $tagNames
+ */
+ private function hasAncestorTag(DOMElement $element, array $tagNames): bool
+ {
+ for ( $node = $element->parentNode; $node instanceof DOMElement && 'body' !== strtolower($node->tagName); $node = $node->parentNode ) {
+ if ( in_array(strtolower($node->tagName), $tagNames, true) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* @param array> $blocks
* @param array> $sourceProvenance
diff --git a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php
index e388f38..6b17011 100644
--- a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php
+++ b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php
@@ -216,6 +216,10 @@ private function navigationItemAttributes(DOMElement $item, DOMElement $anchor,
$itemAttrs = $item->isSameNode($anchor) ? array() : $presentationAttributes($item);
$anchorAttrs = $presentationAttributes($anchor);
$submenuAttrs = $submenuContainer instanceof DOMElement ? $presentationAttributes($submenuContainer) : array();
+ if ( '' === (string) ($itemAttrs['className'] ?? '') && '' !== (string) ($anchorAttrs['className'] ?? '') ) {
+ $itemAttrs['className'] = $anchorAttrs['className'];
+ }
+
return array_filter(array_merge($itemAttrs, $baseAttrs, array(
'anchorClassName' => $anchorAttrs['className'] ?? '',
'anchorStyle' => $anchorAttrs['style'] ?? '',
diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php
index ca693ef..e21c2db 100644
--- a/php-transformer/tests/contract/run.php
+++ b/php-transformer/tests/contract/run.php
@@ -434,6 +434,15 @@ function serialize_blocks(array $blocks): string
$assert(str_contains($footerNavigationSerialized, 'footer-link'), 'footer navigation preserves link classes for styling and script targets');
$assert(str_contains($footerNavigationSerialized, 'social-link'), 'social navigation preserves social link classes for styling and script targets');
+$runtimeTargetNavigation = ( new HtmlTransformer() )->transform(
+ ' ',
+ array('runtime_dom_selectors' => array('.nav-link'))
+)->toArray();
+$runtimeTargetNavigationSerialized = (string) ($runtimeTargetNavigation['serialized_blocks'] ?? '');
+$runtimeTargetNavigationItemAttrs = $runtimeTargetNavigation['blocks'][0]['innerBlocks'][0]['attrs'] ?? array();
+$assert('nav-link' === ($runtimeTargetNavigationItemAttrs['className'] ?? ''), 'runtime-target navigation link classes are preserved on navigation item attrs');
+$assert(str_contains($runtimeTargetNavigationSerialized, ''), 'runtime-target navigation link classes are exposed on navigation item markup');
+
$headerCluster = ( new HtmlTransformer() )->transform(
''
)->toArray();
@@ -456,6 +465,13 @@ function serialize_blocks(array $blocks): string
$assert(1 === ($unmappedFinding['source_count'] ?? null), 'semantic parity missing landmark finding exposes source count');
$assert(0 === ($unmappedFinding['block_count'] ?? null), 'semantic parity missing landmark finding exposes generated block count');
+$quoteCitationFooter = ( new HtmlTransformer() )->transform(
+ ' '
+)->toArray();
+$quoteCitationParity = $quoteCitationFooter['source_reports']['semantic_parity'] ?? array();
+$assert('pass' === ($quoteCitationParity['status'] ?? ''), 'blockquote citation footer is not counted as a page footer landmark');
+$assert(1 === ($quoteCitationParity['landmarks']['source']['footer'] ?? null), 'semantic parity counts only the actual page footer landmark');
+
$assertNoInnerContentChildCountMismatch = static function (array $result, string $message) use ($assert): void {
$findingCodes = array_map(static fn (array $finding): string => (string) ($finding['code'] ?? ''), $result['source_reports']['wp_block_validity']['findings'] ?? array());
$assert(! in_array('inner_content_child_count_mismatch', $findingCodes, true), $message, implode(', ', $findingCodes));