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
1 change: 1 addition & 0 deletions php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 2 additions & 5 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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' ))
);
}

/**
Expand Down
26 changes: 6 additions & 20 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 `<header>`/`<section>`/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<int, string>
*/
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;
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
88 changes: 88 additions & 0 deletions php-transformer/src/HtmlToBlocks/ShellLandmarkPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks;

/**
* Centralizes the distinction between document shell landmarks and content-local
* semantic containers so transformer/materialization decisions stay aligned.
*/
final class ShellLandmarkPolicy
{
/** @var array<int, string> */
private const GLOBAL_SHELL_LANDMARK_TAGS = array( 'header', 'footer', 'nav' );

/** @var array<int, string> */
private const SEMANTIC_GROUP_TAGS = array( 'header', 'nav', 'section', 'article', 'aside', 'footer', 'main' );

/** @var array<int, string> */
private const FLOW_CONTAINER_TAGS = array( 'article', 'aside', 'body', 'center', 'div', 'footer', 'header', 'main', 'nav', 'section' );

/** @var array<int, string> */
private const WRAPPER_PRESERVING_TAGS = array( 'article', 'aside', 'div', 'footer', 'header', 'main', 'nav', 'section' );

/** @var array<int, string> */
private const INLINE_TOKEN_CONTAINER_TAGS = array( 'div', 'footer', 'header', 'main', 'nav', 'section' );

/** @var array<int, string> */
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';
}
}
46 changes: 46 additions & 0 deletions php-transformer/tests/unit/shell-landmark-policy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);

require dirname(__DIR__, 2) . '/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\ShellLandmarkPolicy;

$failures = 0;
$passes = 0;

$assert = static function (bool $condition, string $message, string $detail = '') use (&$failures, &$passes): void {
if ( $condition ) {
++$passes;
return;
}

++$failures;
fwrite(STDERR, 'FAIL: ' . $message . ('' !== $detail ? ' - ' . $detail : '') . PHP_EOL);
};

$assert(ShellLandmarkPolicy::isGlobalShellLandmarkTag('header'), 'header is global shell');
$assert(ShellLandmarkPolicy::isGlobalShellLandmarkTag('footer'), 'footer is global shell');
$assert(ShellLandmarkPolicy::isGlobalShellLandmarkTag('nav'), 'nav is global shell');
$assert(! ShellLandmarkPolicy::isGlobalShellLandmarkTag('main'), 'main remains page/content-local, not reusable shell');
$assert(! ShellLandmarkPolicy::isGlobalShellLandmarkTag('article'), 'article remains content-local');

$assert('footer' === ShellLandmarkPolicy::landmarkKind('footer'), 'plain footer is a footer landmark');
$assert('' === ShellLandmarkPolicy::landmarkKind('footer', '', true), 'blockquote/figure footer is content-local citation, not page footer');
$assert('header' === ShellLandmarkPolicy::landmarkKind('div', 'banner'), 'role banner maps to header landmark');
$assert('nav' === ShellLandmarkPolicy::landmarkKind('div', 'navigation'), 'role navigation maps to nav landmark');
$assert('main' === ShellLandmarkPolicy::landmarkKind('main'), 'main maps to main landmark');

$assert(ShellLandmarkPolicy::isSemanticGroupTag('main'), 'main can remain a semantic core/group tag');
$assert(ShellLandmarkPolicy::isWrapperPreservingTag('main'), 'main wrapper can preserve source style/structure');
$assert(ShellLandmarkPolicy::isInlineContentWrapperTag('footer'), 'footer can still be content-local phrasing wrapper');

$assert('header' === ShellLandmarkPolicy::templatePartArea('parts/header.html', ''), 'header template part area comes from path');
$assert('footer' === ShellLandmarkPolicy::templatePartArea('parts/site-shell.html', 'template-part footer'), 'footer template part area comes from role');
$assert('uncategorized' === ShellLandmarkPolicy::templatePartArea('pages/main.html', ''), 'main-named content is not promoted to a template part area');

if ( $failures > 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);
Loading