diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index 9817f4ee..75f51073 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -2012,7 +2012,10 @@ private function compiledSiteReport(array $artifact, string $entryPath, array $d $shellArtifact['slug'] = 'entry-' . $slug; } $partSlugs[(string) $shellArtifact['slug']] = true; - if ( is_string($shellArtifact['inner_block_markup'] ?? null) ) { + if ( is_string($shellArtifact['template_part_block_markup'] ?? null) ) { + $shellArtifact['block_markup'] = $shellArtifact['template_part_block_markup']; + unset($shellArtifact['template_part_block_markup'], $shellArtifact['inner_block_markup']); + } elseif ( is_string($shellArtifact['inner_block_markup'] ?? null) ) { $shellArtifact['block_markup'] = $shellArtifact['inner_block_markup']; unset($shellArtifact['inner_block_markup']); } diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index b5ec4f65..f1ecb3ee 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -662,6 +662,11 @@ private function globalShellArtifacts(DOMElement $body, string $source, bool $re // landmark around an independently converted landmark block. $blocks = array($this->createBlock('core/group', $wrapperAttrs, $blocks, $child)); $markup = $this->runtime->serializeBlocks($blocks); + $templatePartAttrs = $wrapperAttrs; + unset($templatePartAttrs['tagName']); + $templatePartMarkup = array() === $templatePartAttrs + ? $innerMarkup + : $this->runtime->serializeBlocks(array($this->createBlock('core/group', $templatePartAttrs, $blocks[0]['innerBlocks'] ?? array()))); if ( '' === trim($markup) ) { continue; } @@ -673,6 +678,7 @@ private function globalShellArtifacts(DOMElement $body, string $source, bool $re 'body_format' => 'blocks', 'block_markup' => $markup, 'inner_block_markup' => $innerMarkup, + 'template_part_block_markup' => $templatePartMarkup, 'source_selector' => strtolower($child->tagName), 'source_classes' => $this->shellSourceClasses($child), 'source_hash' => hash('sha256', $this->outerHtml($child)), diff --git a/php-transformer/src/WordPressSitePlan/WordPressSitePlan.php b/php-transformer/src/WordPressSitePlan/WordPressSitePlan.php index 81b76787..60a5d768 100644 --- a/php-transformer/src/WordPressSitePlan/WordPressSitePlan.php +++ b/php-transformer/src/WordPressSitePlan/WordPressSitePlan.php @@ -247,7 +247,8 @@ private function shellCandidates(array $document, AssetReferenceCanonicalizer $r $classes = array_values(array_filter($candidate['source_classes'] ?? array(), 'is_string')); sort($classes, SORT_STRING); $innerMarkup = is_string($candidate['inner_block_markup'] ?? null) ? $this->routeLinks($references->content($candidate['inner_block_markup'], self::value($document, 'source_path')), self::value($document, 'source_path'), $routes) : $markup; - $candidates[] = array('area' => $candidate['area'], 'markup' => $markup, 'inner_markup' => $innerMarkup, 'classes' => $classes); + $templatePartMarkup = is_string($candidate['template_part_block_markup'] ?? null) ? $this->routeLinks($references->content($candidate['template_part_block_markup'], self::value($document, 'source_path')), self::value($document, 'source_path'), $routes) : $innerMarkup; + $candidates[] = array('area' => $candidate['area'], 'markup' => $markup, 'inner_markup' => $innerMarkup, 'template_part_markup' => $templatePartMarkup, 'classes' => $classes); } return $candidates; } @@ -317,7 +318,7 @@ private function sharedShells(array $pages, array $reservedSlugs = array(), arra $sourcePath = $singlePage ? $pages[array_key_first($applicable)]['source_path'] : 'wordpress-site-plan/shared/' . $area; $placement = $singlePage ? 'entry_shell' : 'shared_shell'; $templateSlugs = $singlePage ? array('front-page') : array('index', 'page', 'front-page'); - $partMarkup = $first['markup']; + $partMarkup = $first['template_part_markup']; $parts[] = array('source_path' => $sourcePath . '#' . $area, 'slug' => $area, 'title' => ucfirst($area), 'post_type' => 'wp_template_part', 'parent_source_path' => '', 'entrypoint' => false, 'area' => $area, 'placement' => array('kind' => $placement, 'source_path' => $sourcePath, 'template_slugs' => $templateSlugs), 'canonical_block_markup' => $partMarkup, 'metadata' => array(), 'document_metadata' => array('source_context' => array('source_path' => $sourcePath . '#' . $area, 'kind' => 'template_part'), 'title' => ucfirst($area), 'title_declaration' => array('order' => 0, 'placement' => 'head'), 'meta' => array(), 'links' => array(), 'scripts' => array()), 'provenance' => array('shell_identity' => $identity), 'reconciliation_identity' => self::identity('template-part', $sourcePath . '#' . $area, 'parts/' . $area . '.html'), 'content_hash' => self::contentHash($partMarkup)); $diagnostics[] = array('code' => $singlePage ? 'wordpress_site_plan_shell_entry_extracted' : 'wordpress_site_plan_shell_extracted', 'severity' => 'info', 'message' => $singlePage ? "Extracted the entry {$area} shell for the front-page template." : "Extracted one semantically equivalent {$area} shell for all pages.", 'area' => $area, 'page_count' => count($applicable)); } @@ -491,7 +492,7 @@ private function templates(array $pages, array $parts): array $markup = static function (string $templateSlug) use ($bound): string { $before = ''; $after = ''; foreach ($bound as $part) if (in_array($templateSlug, $part['placement']['template_slugs'] ?? array(), true)) { - $reference = '' . "\n"; + $reference = '' . "\n"; if ('footer' === $part['area']) $after .= $reference; else $before .= $reference; } return $before . '' . "\n" . $after; diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 205f8640..56eb0a20 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -2140,6 +2140,7 @@ public function match(DOMElement $element, PatternContext $context): ?array $canonicalEntryPage = array_values(array_filter($canonicalShellPlan['pages'] ?? array(), static fn (array $page): bool => 'index.html' === ($page['source_path'] ?? '')))[0] ?? array(); $assert(! str_contains((string) ($canonicalEntryPage['canonical_block_markup'] ?? ''), 'Get started') && str_contains((string) ($canonicalHeaderPart['canonical_block_markup'] ?? ''), 'Get started'), 'canonical entry header is projected only to its shell part, without duplicate post-content chrome'); $assert(str_contains((string) ($canonicalFooterPart['canonical_block_markup'] ?? ''), 'Global footer'), 'canonical entry footer part preserves global footer content'); +$assert(! str_contains((string) ($canonicalHeaderPart['canonical_block_markup'] ?? ''), ' 'theme_template_part' === ($write['kind'] ?? ''))), 'WordPress site plan exposes canonical entry header and footer writes'); $runtimeDependencySite = $compiler->compile( diff --git a/php-transformer/tests/contract/shared-shell-plan.php b/php-transformer/tests/contract/shared-shell-plan.php index 9f452349..574d9d99 100644 --- a/php-transformer/tests/contract/shared-shell-plan.php +++ b/php-transformer/tests/contract/shared-shell-plan.php @@ -42,15 +42,17 @@ $nearPages = $pages($nearMatch); $assert(str_contains($nearPages['index.html']['canonical_block_markup'] ?? '', 'Home') && str_contains($nearPages['about.html']['canonical_block_markup'] ?? '', 'Contact us') && !array_filter($nearMatch['template_parts'], static fn(array $part): bool => 'header' === ($part['area'] ?? null)), 'Route-specific header actions are retained as a near-match instead of being deduplicated.'); -$singleResult = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '

Solo

Home
')))->toArray(); +$singleResult = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '

Solo

Home
')))->toArray(); $single = $singleResult['source_reports']['wordpress_site_plan']; $singleWrites = $writes($single); -$assert(2 === count(array_filter($single['template_parts'], static fn(array $part): bool => 'entry_shell' === ($part['placement']['kind'] ?? null))) && str_contains($singleWrites['templates/front-page.html']['payload']['data'], '"slug":"header"') && !str_contains($singleWrites['templates/page.html']['payload']['data'], '"slug":"header"') && !str_contains($singleWrites['templates/index.html']['payload']['data'], '"slug":"header"'), 'A single-page artifact retains entry-shell behavior and binds chrome only to front-page.'); +$assert(2 === count(array_filter($single['template_parts'], static fn(array $part): bool => 'entry_shell' === ($part['placement']['kind'] ?? null))) && str_contains($singleWrites['templates/front-page.html']['payload']['data'], '"slug":"header","area":"header","tagName":"header"') && str_contains($singleWrites['templates/front-page.html']['payload']['data'], '"slug":"footer","area":"footer","tagName":"footer"') && !str_contains($singleWrites['templates/page.html']['payload']['data'], '"slug":"header"') && !str_contains($singleWrites['templates/index.html']['payload']['data'], '"slug":"header"'), 'A single-page artifact binds semantic entry-shell template parts only to front-page.'); $singleHeader = array_values(array_filter($single['template_parts'], static fn(array $part): bool => 'header' === ($part['area'] ?? null)))[0] ?? array(); -$assert(str_contains($singleHeader['canonical_block_markup'] ?? '', '"tagName":"header"') && str_contains($singleHeader['canonical_block_markup'] ?? '', 'solo') && str_contains($singleHeader['canonical_block_markup'] ?? '', 'Solo') && !str_contains($pages($single)['index.html']['canonical_block_markup'] ?? '', 'Solo

'), 'Single-page entry chrome preserves its semantic landmark and source presentation while removing it from post content.'); +$assert(!str_contains($singleHeader['canonical_block_markup'] ?? '', '"tagName":"header"') && !str_contains($singleHeader['canonical_block_markup'] ?? '', ''), 'Single-page entry chrome preserves source presentation without nesting a landmark inside the semantic template-part wrapper.'); $compiledHeader = array_values(array_filter($singleResult['source_reports']['compiled_site']['template_parts'] ?? array(), static fn(array $part): bool => 'header' === ($part['area'] ?? null)))[0] ?? array(); $compiledPage = $singleResult['source_reports']['compiled_site']['pages'][0] ?? array(); -$assert('entry_shell' === ($compiledHeader['placement']['kind'] ?? null) && !str_contains($compiledHeader['block_markup'] ?? '', '"tagName":"header"') && str_contains($compiledHeader['block_markup'] ?? '', 'Solo') && !str_contains($compiledPage['block_markup'] ?? '', 'Solo

'), 'The compiled-site compatibility report retains its established single-page entry shell projection without duplicate page chrome.'); +$assert('entry_shell' === ($compiledHeader['placement']['kind'] ?? null) && !str_contains($compiledHeader['block_markup'] ?? '', '"tagName":"header"') && !str_contains($compiledHeader['block_markup'] ?? '', ''), 'The compiled-site compatibility report retains source presentation on a non-landmark wrapper without duplicate page chrome.'); +$materializedHeader = array_values(array_filter($singleResult['source_reports']['materialization_plan']['template_part_writes'] ?? array(), static fn(array $write): bool => 'header' === ($write['area'] ?? null)))[0] ?? array(); +$assert(str_contains($materializedHeader['content'] ?? '', '"className":"solo"') && str_contains($materializedHeader['content'] ?? '', '"anchor":"solo-shell"') && str_contains($materializedHeader['content'] ?? '', 'border-top:2px solid #111') && str_contains($materializedHeader['content'] ?? '', 'Solo') && !str_contains($materializedHeader['content'] ?? '', '"tagName":"header"') && !str_contains($materializedHeader['content'] ?? '', 'compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '
Shared
Home
', 'about.html' => '
Shared
About
', 'contact.html' => '
Contact
')))->toArray()['source_reports']['wordpress_site_plan']; $multiple = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '
One
Two
Home
', 'about.html' => '
One
Two
About
')))->toArray()['source_reports']['wordpress_site_plan'];