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
21 changes: 13 additions & 8 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2278,9 +2278,11 @@ private function applyAuthorStylesheetProjections(array $files, array $projectio
$sharedStylesheets = array_merge(array_map('strval', array_keys($sharedByPath[$path] ?? array())), $stylesheets);
// A page's projection repeats every rule it did not rewrite. Only the
// rules missing from the site-wide copy go to a stylesheet the page
// alone loads, placed before the site-wide copy: every page already
// saw its own rules ahead of the authoritative copy, so the cascade is
// unchanged while each page stops shipping its projection site-wide.
// alone loads. Place it after the site-wide copy: the latter may
// contain a generic selector that still matches the lowered block
// (e.g. a grid's default two-column rule), while the page projection
// carries the source's later attribute-specific responsive winner.
$output[] = $this->projectedStylesheetFile($file, $path, implode("\n", array_merge($preambles, array( $transformer->concatenateWithoutRedundantRules($sharedStylesheets) ))));
foreach ( $pages as $owner => $pageStylesheets ) {
$delta = $transformer->rulesAbsentFrom($pageStylesheets, $sharedStylesheets);
if ( '' === trim($delta) ) {
Expand All @@ -2291,7 +2293,6 @@ private function applyAuthorStylesheetProjections(array $files, array $projectio
$pageFile['metadata']['page_stylesheet_of'] = $path;
$output[] = $pageFile;
}
$output[] = $this->projectedStylesheetFile($file, $path, implode("\n", array_merge($preambles, array( $transformer->concatenateWithoutRedundantRules($sharedStylesheets) ))));
}
return $output;
}
Expand Down Expand Up @@ -4412,15 +4413,19 @@ private function assetManifest(array $files, string $entryPath, array $assetRefe
}
$orderedPaths = array();
$pageStylesheets = array();
$pageStylesheetParents = array();
foreach ( $files as $file ) {
if ( is_string($file['metadata']['page_stylesheet_of'] ?? null) ) {
$pageStylesheets[$file['metadata']['page_stylesheet_of']][] = (string) $file['path'];
$parent = $file['metadata']['page_stylesheet_of'];
$pageStylesheets[$parent][] = (string) $file['path'];
$pageStylesheetParents[(string) $file['path']] = $parent;
}
}
// A page's projected stylesheet loads immediately before the site-wide
// stylesheet it was split from, the position its rules held there.
// The page's attribute-specific projection follows the shared copy at
// the same source stylesheet boundary, before the next linked sheet.
foreach ( array_column($this->stylesheetAssetsForSource($entryHtml, $entryPath, $files), 'path') as $path ) {
array_push($orderedPaths, ...($pageStylesheets[$path] ?? array()), ...array( $path ));
$path = $pageStylesheetParents[$path] ?? $path;
array_push($orderedPaths, $path, ...($pageStylesheets[$path] ?? array()));
}
$ordered = array();
$consumed = array();
Expand Down
1 change: 1 addition & 0 deletions php-transformer/src/HtmlToBlocks/Style/InlineGeometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function layoutCarrierProperties(): array
'align-items',
'justify-content',
'gap',
'grid-gap',
);
}

Expand Down
43 changes: 43 additions & 0 deletions php-transformer/src/WordPressSitePlan/WordPressSitePlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ public function fromCompilerInput(array $data, WordPressSitePlanInput $input): a
$parts = array_merge($existingParts, $inlineShells['parts'], $shells['parts']);
$assets = self::projectSharedChromeStylesheets($assets, $parts, $pages, $references);
$assets = self::projectDetachedChromePaintOrder($assets, $parts);
$assets = self::orderPageStylesheetProjections($assets);
$tokens = $this->tokens($assets);
if (array() !== $parts) $themeProjection['theme']['templateParts'] = array_values(array_map(static fn(array $part): array => array('name' => $part['slug'], 'title' => $part['title'], 'area' => $part['area']), $parts));
$runtimeDeclarations = $shells['runtime_declarations'];
Expand Down Expand Up @@ -1197,6 +1198,48 @@ static function (string $prelude, string $body) use ($targetsChrome): string {
return $projected;
}

/**
* A page-specific projection must follow its source stylesheet in the
* WordPress enqueue list, even when another document's linked CSS forces
* the original stylesheet into a later slot. Keep each detached shared
* chrome slice with its page projection at that same source boundary.
*
* @param array<int,array<string,mixed>> $assets
* @return array<int,array<string,mixed>>
*/
private static function orderPageStylesheetProjections(array $assets): array
{
$sources = array_fill_keys(array_column($assets, 'source_path'), true);
$children = array();
foreach ($assets as $asset) {
$source = (string) ($asset['source_path'] ?? '');
if ('css' !== ($asset['kind'] ?? null)
|| !preg_match('/^(.*)\.page-[a-f0-9]{12}\.css(\.shared-chrome)?$/D', $source, $match)
|| !isset($sources[$match[1] . '.css'])
) {
continue;
}
$children[$match[1] . '.css'][] = $asset;
}
if (array() === $children) return $assets;

$ordered = array();
foreach ($assets as $asset) {
$source = (string) ($asset['source_path'] ?? '');
if (preg_match('/^(.*)\.page-[a-f0-9]{12}\.css(?:\.shared-chrome)?$/D', $source, $match)
&& isset($children[$match[1] . '.css'])
) {
continue;
}
$ordered[] = $asset;
$parent = str_ends_with($source, '.shared-chrome') ? substr($source, 0, -strlen('.shared-chrome')) : $source;
if (isset($children[$parent]) && ($source === $parent . '.shared-chrome' || !isset($sources[$parent . '.shared-chrome']))) {
array_push($ordered, ...$children[$parent]);
}
}
return $ordered;
}

/** @param array<string,mixed> $asset */
private static function sharedChromeContract(array $asset): string
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,65 @@
$assert(1 === count($multiPageSupportAssets), 'identical generated engine support stylesheets are emitted once across HTML routes');
$multiPageAssetPaths = array_column($multiPage['assets'] ?? array(), 'path');
$multiPageWordPressAssets = $multiPage['source_reports']['wordpress_site_plan']['assets'] ?? array();
$assert(1 === preg_match('#^assets/css/engine-support-before-author-[a-f0-9]{16}\.css$#', $multiPageAssetPaths[0] ?? '') && 'shared.css' === ($multiPageAssetPaths[2] ?? '') && 'shared' === ($multiPageSupportAssets[0]['compilation']['scope'] ?? '') && 'global' === ($multiPageWordPressAssets[0]['scopes'][0]['kind'] ?? ''), 'identical multi-page support is ordered before author CSS and promoted to global scope');
$multiPageAboutCss = $multiPage['assets'][1] ?? array();
$assert(1 === preg_match('#^shared\.page-[a-f0-9]{12}\.css$#', (string) ($multiPageAboutCss['path'] ?? '')) && array( 'scope' => 'page', 'id' => 'about.html' ) === ($multiPageAboutCss['compilation'] ?? null) && 'page' === ($multiPageWordPressAssets[1]['scopes'][0]['kind'] ?? '') && ! str_contains((string) ($multiPage['assets'][2]['content'] ?? ''), (string) preg_replace('/^.*?(blocks-engine-richtext-[a-f0-9]{12})-.*$/s', '$1', (string) ($multiPageAboutCss['content'] ?? ''))), 'a page\'s own projection of a shared stylesheet is page-scoped, loads just before it, and stays out of the site-wide copy');
$assert(1 === preg_match('#^assets/css/engine-support-before-author-[a-f0-9]{16}\.css$#', $multiPageAssetPaths[0] ?? '') && 'shared.css' === ($multiPageAssetPaths[1] ?? '') && 'shared' === ($multiPageSupportAssets[0]['compilation']['scope'] ?? '') && 'global' === ($multiPageWordPressAssets[0]['scopes'][0]['kind'] ?? ''), 'identical multi-page support is ordered before author CSS and promoted to global scope');
$multiPageAboutCss = $multiPage['assets'][2] ?? array();
$assert(1 === preg_match('#^shared\.page-[a-f0-9]{12}\.css$#', (string) ($multiPageAboutCss['path'] ?? '')) && array( 'scope' => 'page', 'id' => 'about.html' ) === ($multiPageAboutCss['compilation'] ?? null) && 'page' === ($multiPageWordPressAssets[2]['scopes'][0]['kind'] ?? '') && ! str_contains((string) ($multiPage['assets'][1]['content'] ?? ''), (string) preg_replace('/^.*?(blocks-engine-richtext-[a-f0-9]{12})-.*$/s', '$1', (string) ($multiPageAboutCss['content'] ?? ''))), 'a page\'s own projection of a shared stylesheet is page-scoped, loads after it, and stays out of the site-wide copy');
$assert('blocks-engine/wordpress-site-plan/v2' === ($multiPage['source_reports']['wordpress_site_plan']['schema'] ?? null), 'deduplicated multi-route assets produce a canonical WordPress site plan');

// A source grid's attribute-specific desktop winner must remain later than a
// shared fallback that still matches the attribute-free editable Group.
$responsiveGrid = ( new ArtifactCompiler() )->compile(array(
'entrypoint' => 'index.html',
'files' => array(
array( 'path' => 'index.html', 'kind' => 'html', 'content' => '<link rel="stylesheet" href="cards.css"><header class="shared">Shared header</header><main><ul class="cards" data-count="2"><li>One</li><li>Two</li></ul></main>' ),
array( 'path' => 'people.html', 'kind' => 'html', 'content' => '<link rel="stylesheet" href="cards.css"><header class="shared">Shared header</header><main><ul class="cards" data-count="4" style="grid-gap:100px 100px"><li><img src="one.jpg" alt="One"></li><li>Two</li><li>Three</li><li>Four</li></ul></main>' ),
array( 'path' => 'cards.css', 'kind' => 'css', 'content' => '.shared{color:#456}.cards{display:grid;gap:20px;grid-template-columns:1fr}.cards:not([data-count="1"]){grid-template-columns:repeat(2,1fr)}@media(min-width:768px){.cards[data-count="4"]{grid-template-columns:repeat(3,1fr)}}@media(min-width:1100px){.cards[data-count="4"]{grid-template-columns:repeat(4,1fr)}}@media(max-width:600px){.cards[data-count="4"]{grid-template-columns:1fr}}' ),
array( 'path' => 'one.jpg', 'kind' => 'image', 'content' => 'image-bytes' ),
),
) )->toArray();
$gridAssets = $responsiveGrid['assets'] ?? array();
$gridBaseIndex = array_search('cards.css', array_column($gridAssets, 'path'), true);
$gridPageAssets = array_values(array_filter($gridAssets, static fn (array $asset): bool => str_starts_with((string) ($asset['path'] ?? ''), 'cards.page-')));
$gridPageCss = implode('', array_column($gridPageAssets, 'content'));
$gridBaseCss = false !== $gridBaseIndex ? (string) ($gridAssets[$gridBaseIndex]['content'] ?? '') : '';
$gridMarker = '';
preg_match('/blocks-engine-attribute-[a-f0-9]+-\d+/', $gridPageCss, $gridMarkerMatch);
$gridMarker = $gridMarkerMatch[0] ?? '';
$gridPages = array_column($responsiveGrid['source_reports']['compiled_site']['pages'] ?? array(), null, 'source_path');
$gridMarkup = (string) ($gridPages['people.html']['block_markup'] ?? '');
$assert(false !== $gridBaseIndex && count($gridPageAssets) === 1 && array_search($gridPageAssets[0]['path'], array_column($gridAssets, 'path'), true) > $gridBaseIndex, 'the page-specific grid projection follows its shared stylesheet');
$gridPlanPaths = array_column($responsiveGrid['source_reports']['wordpress_site_plan']['assets'] ?? array(), 'source_path');
$gridPlanBase = array_search('cards.css', $gridPlanPaths, true);
$gridPlanPage = array_search($gridPageAssets[0]['path'] ?? '', $gridPlanPaths, true);
$assert(false !== $gridPlanBase && false !== $gridPlanPage && $gridPlanBase < $gridPlanPage, 'the WordPress site plan preserves the shared-then-page stylesheet cascade through theme enqueue');
$gridSource = 'cards.css';
$gridPage = 'cards.page-123456789abc.css';
$orderedGridStyles = ( new ReflectionMethod(\Automattic\BlocksEngine\PhpTransformer\WordPressSitePlan\WordPressSitePlan::class, 'orderPageStylesheetProjections') )->invoke(null, array(
array('kind' => 'css', 'source_path' => $gridPage, 'content' => '.cards:not([data-count="1"]){grid-template-columns:repeat(4,1fr)}'),
array('kind' => 'css', 'source_path' => $gridPage . '.shared-chrome', 'content' => '.shared{color:#456}'),
array('kind' => 'css', 'source_path' => 'unrelated.css', 'content' => '.other{color:#789}'),
array('kind' => 'css', 'source_path' => $gridSource, 'content' => '.cards:not([data-count="1"]){grid-template-columns:repeat(2,1fr)}'),
array('kind' => 'css', 'source_path' => $gridSource . '.shared-chrome', 'content' => '.shared{color:#123}'),
));
$orderedGridSources = array_column($orderedGridStyles, 'source_path');
$assert(array('unrelated.css', $gridSource, $gridSource . '.shared-chrome', $gridPage, $gridPage . '.shared-chrome') === $orderedGridSources, 'a linked shared stylesheet stays before its page-scoped winner after chrome factoring without moving unrelated CSS');
$assert('' !== $gridMarker && str_contains($gridPageCss, 'grid-template-columns:repeat(4,1fr)') && str_contains($gridPageCss, '@media(max-width:600px)') && str_contains($gridBaseCss, 'grid-template-columns:repeat(2,1fr)'), 'desktop and mobile grid variants retain their authored media conditions against the shared fallback');
$assert(str_contains(implode("\n", array_column($gridAssets, 'content')), 'grid-gap:100px 100px') && str_contains($gridMarkup, 'be-inline-geometry-'), 'the editable grid keeps its inline-authored legacy gap through the existing layout carrier');
$assert(str_contains($gridMarkup, 'blocks-engine-css-owned-grid') && str_contains($gridMarkup, $gridMarker) && 'pass' === ( new Runtime() )->validateBlockSerialization($gridMarkup)['status'], 'the attribute-selected four-card grid remains a valid editable Group document');
$gridDocument = new DOMDocument();
@$gridDocument->loadHTML('<body>' . preg_replace('/<!--.*?-->/s', '', $gridMarkup) . '</body>');
$gridElement = null;
foreach ( $gridDocument->getElementsByTagName('ul') as $candidate ) {
if ( str_contains($candidate->getAttribute('class'), $gridMarker) ) {
$gridElement = $candidate;
break;
}
}
$gridCascade = new StaticCssCascade($gridDocument, $gridBaseCss . $gridPageCss);
$assert($gridElement instanceof DOMElement && 'repeat(4,1fr)' === ($gridCascade->resolve($gridElement, array( 'grid-template-columns' ), array())['grid-template-columns'] ?? ''), 'the projected four-column rule wins the shared two-column fallback in source order at the desktop reference');
$gridCascade = new StaticCssCascade($gridDocument, implode("\n", array_column($orderedGridStyles, 'content')));
$assert('repeat(4,1fr)' === ($gridCascade->resolve($gridElement, array('grid-template-columns'), array())['grid-template-columns'] ?? ''), 'the final site-plan stylesheet order preserves the four-column browser winner');

$pageSubsetArtifact = array(
'entrypoint' => 'index.html',
'files' => array(
Expand Down
Loading