diff --git a/php-transformer/composer.json b/php-transformer/composer.json index 566ce4e2..965888e8 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -62,6 +62,7 @@ "php tests/unit/button-signal-classifier.php", "php tests/unit/button-style-resolver.php", "php tests/unit/button-visual-probe-diagnostics.php", + "php tests/unit/shell-landmark-policy.php", "php tests/unit/block-style-support-conversion.php", "php tests/unit/content-round-trip-reporter.php", "php tests/unit/content-round-trip-form-echo.php", diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index e934cc70..bdf91632 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -8,6 +8,7 @@ use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult; use Automattic\BlocksEngine\PhpTransformer\FormatBridge\FormatBridge; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer; +use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\ShellLandmarkPolicy; use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath; use Automattic\BlocksEngine\PhpTransformer\StaticSite\MaterializationPlanBuilder; use DOMDocument; @@ -1713,11 +1714,7 @@ private function isTemplatePartFile(array $file): bool private function templatePartArea(string $path, string $role): string { - if ( preg_match('/\b(header|footer|sidebar|navigation)\b/i', $path . ' ' . $role, $match) ) { - return strtolower($match[1]); - } - - return 'uncategorized'; + return ShellLandmarkPolicy::templatePartArea($path, $role); } /** diff --git a/php-transformer/src/HtmlToBlocks/Diagnostics/SemanticParityReporter.php b/php-transformer/src/HtmlToBlocks/Diagnostics/SemanticParityReporter.php index 9d5b54dd..1805cbc2 100644 --- a/php-transformer/src/HtmlToBlocks/Diagnostics/SemanticParityReporter.php +++ b/php-transformer/src/HtmlToBlocks/Diagnostics/SemanticParityReporter.php @@ -4,6 +4,7 @@ namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Diagnostics; use Automattic\BlocksEngine\PhpTransformer\Contract\ConversionFindingContract; +use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\ShellLandmarkPolicy; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Support\DomHelpersTrait; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\TypographyParityAnalyzer; use Automattic\BlocksEngine\PhpTransformer\WordPress\Runtime; @@ -122,22 +123,11 @@ 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; - } - - return match ( strtolower($this->attr($element, 'role')) ) { - 'banner' => 'header', - 'navigation' => 'nav', - 'main' => 'main', - 'contentinfo' => 'footer', - default => '', - }; + return ShellLandmarkPolicy::landmarkKind( + $element->tagName, + $this->attr($element, 'role'), + $this->hasAncestorTag($element, array( 'blockquote', 'figure' )) + ); } /** diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index b3272675..0fb5d73c 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -1346,7 +1346,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca } } - if ( in_array($tagName, array( 'article', 'aside', 'body', 'center', 'div', 'footer', 'header', 'main', 'nav', 'section' ), true) ) { + if ( ShellLandmarkPolicy::isFlowContainerTag($tagName) ) { if ( $this->shouldPreserveRuntimeAppShell($element) ) { $targets = $this->runtimeTargetsInSubtree($element, 8); $this->recordRuntimeIsland($element, 'app_shell', 'runtime_app_shell', 'client_script_execution', array( @@ -2083,25 +2083,11 @@ private function unwrapElement(DOMElement $element): void $parent->removeChild($element); } - /** - * Semantic HTML5 container tags core's `core/group` block can render as its - * wrapper via the `tagName` attribute. A source `
`/`
`/etc. - * collapsed to a group keeps its real tag so tag-qualified source CSS - * (`header { ... }`, `section { ... }`) still matches the rendered output. - * - * `figure` is intentionally excluded — it has its own conversion path and is - * not a core/group wrapper option. Nav link lists are handled separately by - * the navigation path and never reach here. - * - * @var array - */ - private const SEMANTIC_GROUP_TAGS = array( 'header', 'nav', 'section', 'article', 'aside', 'footer', 'main' ); - private function semanticGroupTagName(DOMElement $element): ?string { $tag = strtolower($element->tagName); - return in_array($tag, self::SEMANTIC_GROUP_TAGS, true) ? $tag : null; + return ShellLandmarkPolicy::isSemanticGroupTag($tag) ? $tag : null; } /** @@ -2282,7 +2268,7 @@ private function recordStructureProvenance(string $blockName, array $attrs, DOME private function shouldPreserveWrapper(DOMElement $element): bool { - return in_array(strtolower($element->tagName), array( 'article', 'aside', 'div', 'footer', 'header', 'main', 'nav', 'section' ), true) && ( $this->isRuntimeDomTarget($element) || array() !== $this->presentationAttributes($element) || array() !== $this->structureSignals($element, array()) ); + return ShellLandmarkPolicy::isWrapperPreservingTag($element->tagName) && ( $this->isRuntimeDomTarget($element) || array() !== $this->presentationAttributes($element) || array() !== $this->structureSignals($element, array()) ); } private function shouldDeferNavigationPatternToChildren(DOMElement $element): bool @@ -2453,7 +2439,7 @@ private function stripDecorativeSvgFromRichText(string $content): string private function inlineTokenGroupBlockFromElement(DOMElement $element, array &$fallbacks): ?array { - if ( ! in_array(strtolower($element->tagName), array( 'div', 'footer', 'header', 'main', 'nav', 'section' ), true) ) { + if ( ! ShellLandmarkPolicy::isInlineTokenContainerTag($element->tagName) ) { return null; } @@ -2587,7 +2573,7 @@ private function hasVisualTextWrapperSignal(DOMElement $element): bool private function paragraphBlockFromInlineContentWrapper(DOMElement $element): ?array { - if ( ! in_array(strtolower($element->tagName), array( 'article', 'div', 'footer', 'header', 'main', 'section' ), true) ) { + if ( ! ShellLandmarkPolicy::isInlineContentWrapperTag($element->tagName) ) { return null; } @@ -4466,7 +4452,7 @@ private function shouldPreserveRuntimeAppShell(DOMElement $element): bool } $tagName = strtolower($element->tagName); - if ( in_array($tagName, array( 'header', 'footer', 'nav' ), true) ) { + if ( ShellLandmarkPolicy::isGlobalShellLandmarkTag($tagName) ) { return false; } diff --git a/php-transformer/src/HtmlToBlocks/ShellLandmarkPolicy.php b/php-transformer/src/HtmlToBlocks/ShellLandmarkPolicy.php new file mode 100644 index 00000000..0ca44603 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/ShellLandmarkPolicy.php @@ -0,0 +1,88 @@ + */ + private const GLOBAL_SHELL_LANDMARK_TAGS = array( 'header', 'footer', 'nav' ); + + /** @var array */ + private const SEMANTIC_GROUP_TAGS = array( 'header', 'nav', 'section', 'article', 'aside', 'footer', 'main' ); + + /** @var array */ + private const FLOW_CONTAINER_TAGS = array( 'article', 'aside', 'body', 'center', 'div', 'footer', 'header', 'main', 'nav', 'section' ); + + /** @var array */ + private const WRAPPER_PRESERVING_TAGS = array( 'article', 'aside', 'div', 'footer', 'header', 'main', 'nav', 'section' ); + + /** @var array */ + private const INLINE_TOKEN_CONTAINER_TAGS = array( 'div', 'footer', 'header', 'main', 'nav', 'section' ); + + /** @var array */ + private const INLINE_CONTENT_WRAPPER_TAGS = array( 'article', 'div', 'footer', 'header', 'main', 'section' ); + + public static function isGlobalShellLandmarkTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::GLOBAL_SHELL_LANDMARK_TAGS, true); + } + + public static function isSemanticGroupTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::SEMANTIC_GROUP_TAGS, true); + } + + public static function isFlowContainerTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::FLOW_CONTAINER_TAGS, true); + } + + public static function isWrapperPreservingTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::WRAPPER_PRESERVING_TAGS, true); + } + + public static function isInlineTokenContainerTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::INLINE_TOKEN_CONTAINER_TAGS, true); + } + + public static function isInlineContentWrapperTag(string $tagName): bool + { + return in_array(strtolower($tagName), self::INLINE_CONTENT_WRAPPER_TAGS, true); + } + + public static function landmarkKind(string $tagName, string $role = '', bool $insideContentLocalCitation = false): string + { + $tagName = strtolower($tagName); + if ( 'footer' === $tagName && $insideContentLocalCitation ) { + return ''; + } + + if ( in_array($tagName, array( 'header', 'nav', 'main', 'footer' ), true) ) { + return 'nav' === $tagName ? 'nav' : $tagName; + } + + return match ( strtolower($role) ) { + 'banner' => 'header', + 'navigation' => 'nav', + 'main' => 'main', + 'contentinfo' => 'footer', + default => '', + }; + } + + public static function templatePartArea(string $path, string $role): string + { + if ( preg_match('/\b(header|footer|sidebar|navigation)\b/i', $path . ' ' . $role, $match) ) { + return strtolower($match[1]); + } + + return 'uncategorized'; + } +} diff --git a/php-transformer/tests/unit/shell-landmark-policy.php b/php-transformer/tests/unit/shell-landmark-policy.php new file mode 100644 index 00000000..0e1104b0 --- /dev/null +++ b/php-transformer/tests/unit/shell-landmark-policy.php @@ -0,0 +1,46 @@ + 0 ) { + fwrite(STDERR, PHP_EOL . "ShellLandmarkPolicy unit tests: {$passes} passed, {$failures} FAILED" . PHP_EOL); + exit(1); +} + +fwrite(STDOUT, "ShellLandmarkPolicy unit tests: {$passes} passed" . PHP_EOL);