diff --git a/php-transformer/README.md b/php-transformer/README.md index 29484f3..562816a 100644 --- a/php-transformer/README.md +++ b/php-transformer/README.md @@ -108,7 +108,7 @@ The result envelope includes generic `metrics` for wrapper reporting: `input_byt `source_reports.html.source_provenance` exposes bounded source context for converted blocks: selector, safe source attributes, sanitized source fragment, ancestor context, nearby heading text, safe `data-*` attributes, and generic structure hints such as card-like or grid-like wrappers. `source_reports.html.structure_signals` records those card/grid/static layout hints separately so callers can inspect them without relying on block attributes. -`source_reports.conversion_report` exposes a compact generic projection for wrappers that previously reconstructed report slices from lower-level result fields. It includes fallback diagnostics, sanitized fallback context, event attribute projections, source/selector summaries, asset references, navigation candidates, presentation and structure signals, and metrics. It remains product-neutral: callers still own route rewrites, media imports, theme assembly, navigation entity creation, visual repair policy, and acceptance gates. +`source_reports.conversion_report` exposes a compact generic projection for wrappers that previously reconstructed report slices from lower-level result fields. It includes fallback diagnostics, sanitized fallback context, event attribute projections, source/selector summaries, asset references, navigation candidates, presentation and structure signals, and metrics. `source_reports.materialization_plan` exposes generic site-structure planning rows for routes, navigation links, and menus using source paths, target paths/slugs, titles/labels, parent/source relations, order, and kind. These reports remain product-neutral: callers still own route rewrites, media imports, theme assembly, navigation entity creation, visual repair policy, and acceptance gates. `HtmlTransformer` preserves syntax-highlight spans inside `
` when they use safe inline tags and bounded attributes, while plain code remains escaped as text. Figure-wrapped testimonials and quote shapes are normalized to core quote or pullquote blocks with attribution from `cite`, `footer`, or `figcaption` content.
diff --git a/php-transformer/docs/contracts/result-envelope.md b/php-transformer/docs/contracts/result-envelope.md
index a8725b4..7cb4002 100644
--- a/php-transformer/docs/contracts/result-envelope.md
+++ b/php-transformer/docs/contracts/result-envelope.md
@@ -38,6 +38,8 @@ Artifact compiler callers that need a compiled site view should read `source_rep
Artifact compiler callers that write compiled output should read `source_reports.materialization_plan`. Its `assets` list is product-neutral write intent: each row may include `source`, `path`, `target_path`, `kind`, `role`, `intent`, `media_type`, `mime_type`, `bytes`, `binary`, `content_encoding`, `content`, `content_base64`, and `hash`. `target_path` defaults to the normalized artifact-relative `path`; downstream materializers own any product-specific routing, upload, or rewrite policy. Text rows expose `content` with `content_encoding: "text"`; binary rows expose `content_base64` with `content_encoding: "base64"` when the payload is available. Unsafe SVG text is withheld from write rows and reported through diagnostics.
+The materialization plan also includes product-neutral `routes`, `navigation_links`, and `menus` rows derived from compiled pages and navigation markup. These rows use source and target fields such as `source_path`, `target_path`, `target_slug`, `title`, `label`, `parent_source_path`, `source_relation`, `order`, and `kind`; they do not assign WordPress post IDs, terms, menu locations, or persistence policy. Product adapters remain responsible for deciding whether and how to create pages, menus, navigation entities, route rewrites, and imported assets.
+
Transformers may expose `source_reports.conversion_report` as a generic `blocks-engine/php-transformer/conversion-report/v1` projection over the same result envelope. It summarizes fallback diagnostics, source paths/selectors, artifact-local asset references, navigation candidates, presentation-gap signals, and metrics without applying product rewrite, import, routing, or visual-parity policy.
`components` contains inspectable component candidates discovered before materialization. `block_types` contains generated block type artifacts promoted from `block.json` roots. `legacy_mapping` is transitional metadata for consumer-owned migration mappers and is not a long-term package feature commitment.
diff --git a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php
index d8d33b0..ab24c3b 100644
--- a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php
+++ b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php
@@ -29,6 +29,9 @@ public function fromCompiledSite(array $compiledSite): array
$templateParts = $this->templateParts((array) ($compiledSite['template_parts'] ?? array()));
$assets = $this->assets((array) ($compiledSite['assets'] ?? array()));
$visualRepair = is_array($compiledSite['visual_repair'] ?? null) ? $compiledSite['visual_repair'] : array();
+ $routes = $this->routes($pages);
+ $navigationLinks = $this->navigationLinks($pages, $templateParts, $routes);
+ $menus = $this->menus($navigationLinks);
$assetRewriteCandidates = $this->assetRewriteCandidates($pages, $templateParts, $assets);
$plan = array(
@@ -37,6 +40,9 @@ public function fromCompiledSite(array $compiledSite): array
'source_hash' => (string) ($compiledSite['source_hash'] ?? ''),
'entry_path' => (string) ($compiledSite['entry_path'] ?? ''),
'pages' => $pages,
+ 'routes' => $routes,
+ 'navigation_links' => $navigationLinks,
+ 'menus' => $menus,
'template_parts' => $templateParts,
'template_part_writes' => $this->templatePartWrites($templateParts),
'assets' => $assets,
@@ -47,6 +53,9 @@ public function fromCompiledSite(array $compiledSite): array
'products' => is_array($compiledSite['products'] ?? null) ? $compiledSite['products'] : array(),
'totals' => array(
'pages' => count($pages),
+ 'routes' => count($routes),
+ 'navigation_links' => count($navigationLinks),
+ 'menus' => count($menus),
'template_parts' => count($templateParts),
'assets' => count($assets),
),
@@ -67,13 +76,16 @@ private function emptyPlan(): array
return array(
'schema' => self::SCHEMA,
'pages' => array(),
+ 'routes' => array(),
+ 'navigation_links' => array(),
+ 'menus' => array(),
'template_parts' => array(),
'template_part_writes' => array(),
'assets' => array(),
'theme' => array(),
'asset_rewrite_candidates' => array(),
'rewrite_candidates' => array(),
- 'totals' => array('pages' => 0, 'template_parts' => 0, 'assets' => 0),
+ 'totals' => array('pages' => 0, 'routes' => 0, 'navigation_links' => 0, 'menus' => 0, 'template_parts' => 0, 'assets' => 0),
);
}
@@ -102,6 +114,108 @@ private function pages(array $pages): array
return $planned;
}
+ /**
+ * @param array> $pages
+ * @return array>
+ */
+ private function routes(array $pages): array
+ {
+ $routes = array();
+ foreach ( $pages as $index => $page ) {
+ $sourcePath = (string) ($page['source_path'] ?? '');
+ $targetSlug = (string) ($page['slug'] ?? '');
+ if ( '' === $targetSlug && '' !== $sourcePath ) {
+ $targetSlug = $this->slugFromPath($sourcePath);
+ }
+
+ $routes[] = array_filter(array(
+ 'kind' => 'route',
+ 'source_path' => $sourcePath,
+ 'target_path' => $this->routePath($page),
+ 'target_slug' => $targetSlug,
+ 'title' => (string) ($page['title'] ?? ''),
+ 'parent_source_path' => (string) (($page['metadata']['parent_source_path'] ?? '') ?: ''),
+ 'source_relation' => ! empty($page['entrypoint']) ? 'entrypoint' : 'document',
+ 'order' => $index,
+ ), static fn (mixed $value): bool => '' !== $value);
+ }
+
+ return $routes;
+ }
+
+ /**
+ * @param array> $pages
+ * @param array> $templateParts
+ * @param array> $routes
+ * @return array>
+ */
+ private function navigationLinks(array $pages, array $templateParts, array $routes): array
+ {
+ $links = array();
+ $targetSlugsByPath = array();
+ foreach ( $routes as $route ) {
+ $targetPath = (string) ($route['target_path'] ?? '');
+ if ( '' !== $targetPath ) {
+ $targetSlugsByPath[$targetPath] = (string) ($route['target_slug'] ?? '');
+ }
+ }
+
+ foreach ( array('template_part' => $templateParts, 'page' => $pages) as $kind => $documents ) {
+ foreach ( $documents as $document ) {
+ $sourcePath = (string) ($document['source_path'] ?? '');
+ $sourceRelation = 'template_part' === $kind ? 'template_part_navigation' : 'page_navigation';
+ foreach ( $this->anchorLinks((string) ($document['block_markup'] ?? '')) as $index => $anchor ) {
+ $targetPath = $this->targetPathFromHref($anchor['href']);
+ $links[] = array_filter(array(
+ 'kind' => 'navigation_link',
+ 'source_path' => $sourcePath,
+ 'menu_source_path' => $sourcePath,
+ 'target_path' => $targetPath,
+ 'target_slug' => $targetSlugsByPath[$targetPath] ?? $this->slugFromHref($anchor['href']),
+ 'label' => $anchor['label'],
+ 'title' => $anchor['label'],
+ 'parent_source_path' => '',
+ 'source_relation' => $sourceRelation,
+ 'order' => $index,
+ ), static fn (mixed $value): bool => '' !== $value);
+ }
+ }
+ }
+
+ return $this->dedupeRows($links);
+ }
+
+ /**
+ * @param array> $navigationLinks
+ * @return array>
+ */
+ private function menus(array $navigationLinks): array
+ {
+ $menus = array();
+ $orders = array();
+ foreach ( $navigationLinks as $link ) {
+ $sourcePath = (string) ($link['menu_source_path'] ?? $link['source_path'] ?? '');
+ if ( '' === $sourcePath ) {
+ continue;
+ }
+ if ( ! isset($menus[$sourcePath]) ) {
+ $orders[$sourcePath] = count($orders);
+ $menus[$sourcePath] = array(
+ 'kind' => 'menu',
+ 'source_path' => $sourcePath,
+ 'target_slug' => $this->slugFromPath($sourcePath),
+ 'title' => $this->titleFromPath($sourcePath),
+ 'source_relation' => 'navigation_links',
+ 'order' => $orders[$sourcePath],
+ 'items' => 0,
+ );
+ }
+ ++$menus[$sourcePath]['items'];
+ }
+
+ return array_values($menus);
+ }
+
/**
* @param array $templateParts
* @return array>
@@ -245,4 +359,120 @@ private function assetRewriteCandidates(array $pages, array $templateParts, arra
return $candidates;
}
+
+ /**
+ * @param array $page
+ */
+ private function routePath(array $page): string
+ {
+ $sourcePath = (string) ($page['source_path'] ?? '');
+ $slug = (string) ($page['slug'] ?? '');
+ if ( ! empty($page['entrypoint']) || preg_match('#(^|/)index\.[A-Za-z0-9]+$#', $sourcePath) ) {
+ return '/';
+ }
+
+ return '/' . trim('' !== $slug ? $slug : $this->slugFromPath($sourcePath), '/');
+ }
+
+ /**
+ * @return array
+ */
+ private function anchorLinks(string $markup): array
+ {
+ if ( '' === trim($markup) || ! preg_match_all('/