diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 09aad99..7d6a929 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -4080,10 +4080,6 @@ private function hasSidebarAndContentChildren(DOMElement $element): bool */ private function navigationSectionBlockFromElement(DOMElement $element): ?array { - if ( ! $this->hasNavigationContainerSignal($element) ) { - return null; - } - $heading = null; $anchors = array(); foreach ( $element->childNodes as $child ) { @@ -4091,7 +4087,7 @@ private function navigationSectionBlockFromElement(DOMElement $element): ?array continue; } - if ( $child instanceof DOMElement && preg_match('/^h[1-6]$/i', $child->tagName) ) { + if ( $child instanceof DOMElement && $this->isNavigationSectionHeading($child) ) { if ( $heading instanceof DOMElement ) { return null; } @@ -4111,6 +4107,10 @@ private function navigationSectionBlockFromElement(DOMElement $element): ?array return null; } + if ( ! $this->hasNavigationContainerSignal($element) && ! $this->hasSoftNavigationSectionHeadingSignal($heading) ) { + return null; + } + $sectionFallbacks = array(); $blocks = array( $this->convertElement($heading, $sectionFallbacks, true) ); $links = array(); @@ -4126,6 +4126,25 @@ private function navigationSectionBlockFromElement(DOMElement $element): ?array return $this->createBlock('core/group', $this->presentationAttributes($element), array_values(array_filter($blocks)), $element); } + private function isNavigationSectionHeading(DOMElement $element): bool + { + if ( preg_match('/^h[1-6]$/i', $element->tagName) ) { + return true; + } + + if ( ! in_array(strtolower($element->tagName), array( 'div', 'p', 'span' ), true) || '' === trim($element->textContent ?? '') ) { + return false; + } + + $name = strtolower(trim($this->attr($element, 'class') . ' ' . $this->attr($element, 'id') . ' ' . $this->attr($element, 'role') . ' ' . $this->attr($element, 'aria-label'))); + return (bool) preg_match('/(?:^|[\s_-])(?:heading|label|title)(?:$|[\s_-])/', $name); + } + + private function hasSoftNavigationSectionHeadingSignal(DOMElement $element): bool + { + return ! preg_match('/^h[1-6]$/i', $element->tagName) && $this->isNavigationSectionHeading($element); + } + private function hasNavigationContainerSignal(DOMElement $element): bool { if ( 'navigation' === strtolower($this->attr($element, 'role')) ) { diff --git a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php index d753ede..03ced3d 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php @@ -171,6 +171,8 @@ private function navigationLinkBlock(DOMElement $anchor, callable $presentationA private function navigationLabel(string $html): string { + $html = preg_replace('/]*>.*?<\/svg>/is', '', $html) ?? $html; + $html = preg_replace('/]*>\s*<\/span>/i', '', $html) ?? $html; $html = preg_replace('/<([a-z][a-z0-9]*)\b[^>]*\baria-hidden\s*=\s*(["\'])?true\2[^>]*>\s*<\/\1>/i', '', $html) ?? $html; $html = preg_replace('/<\/?(?:' . self::BLOCK_LEVEL_LABEL_TAGS . ')\b[^>]*>/i', '', $html) ?? $html; return trim($html); diff --git a/php-transformer/tests/fixtures/parity/html-footer-labeled-link-clusters.json b/php-transformer/tests/fixtures/parity/html-footer-labeled-link-clusters.json new file mode 100644 index 0000000..b19c3bd --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-footer-labeled-link-clusters.json @@ -0,0 +1,41 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-footer-labeled-link-clusters", + "description": "Converts footer and mobile navigation link clusters introduced by labeled span headings into grouped navigation blocks.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-footer-labeled-link-clusters.json", + "notes": "Generic fixture based on SSI footer CTA/navigation columns and mobile drawer sections." + }, + "legacy_comparison": { + "skip": true, + "reason": "This upstream primitive fixture has no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group", "attrs": { "className": "site-footer" } }, + { "path": "blocks.0.innerBlocks.0", "name": "core/group", "attrs": { "className": "footer-top" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0.innerBlocks.0", "name": "core/paragraph" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0.innerBlocks.1", "name": "core/navigation" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "Shop All", "url": "shop-all.html", "kind": "custom" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.1", "name": "core/group", "attrs": { "className": "mobile-nav__section" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.0", "name": "core/paragraph" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.1", "name": "core/navigation" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.1.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "Help Center", "url": "help.html", "kind": "custom" } } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks", "assert": "count", "count": 2 }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0.innerBlocks.1.innerBlocks", "assert": "count", "count": 3 }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.1.innerBlocks", "assert": "count", "count": 2 }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "serialized_blocks", "assert": "contains", "value": "