From 74336868c2910e8640491d1fcf284a79bd7e9f61 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Wed, 5 Aug 2026 16:06:25 -0400 Subject: [PATCH 1/4] fix(php-transformer): preserve responsive grid geometry ## Summary Responsive CSS grids no longer collapse to single-column stacks after transform. Grids declared as repeat(auto-fit|auto-fill, minmax(W, 1fr)) map to native WordPress grid layout, grids WordPress layout cannot express keep their geometry under a css-owned-grid carrier, and preserved grids retain their gap and container background. ## Why The css-owned demotion introduced by #813 stopped invalid grid/columns combinations but resolved every author-owned grid to a flow group: the layout attribute was unset, no mechanism carried grid-template-columns for inline-styled grids, and hairline-divider grids (gap:1px plus a background painting through the gaps) lost both properties. On real sites every auto-fit gallery, process row, and client strip rendered as a vertical stack. ## How The layout resolver recognizes the one track list native grid layout expresses exactly, repeat(auto-fit|auto-fill, minmax(W, 1fr)), and emits layout {type:grid, minimumColumnWidth:W}; cssOwnedGroupAttributes keeps that layout instead of demoting, and carries the author gap as blockGap plus the container background as a color support so the block stays faithful without the materialized author stylesheet. BlockFactory stops stripping blockGap from grid-layout groups. Non-expressible grids take a new blocks-engine-css-owned-grid class (instead of the flow class) with a :where margin reset, and inline grid declarations ride to the generated stylesheet on the existing inline-geometry carrier. The #813 guard is untouched: grid layouts still decline core/columns. ## Testing - [ ] composer test (parity fixtures, contracts, and unit suites all pass; three new parity fixtures cover the auto-fit mapping, the css-owned-grid carrier, and gap/background carry) --- .../src/HtmlToBlocks/BlockFactory.php | 6 +- .../src/HtmlToBlocks/HtmlTransformer.php | 74 +++++++++++++++++++ .../Style/StyleResolutionTrait.php | 22 ++++++ .../tests/contract/wordpress-site-plan.php | 2 +- ...tifact-inline-style-extraction-base64.json | 4 +- .../artifact-inline-style-extraction.json | 4 +- ...nked-css-layout-wrapper-style-signals.json | 8 +- ...symmetric-grid-css-owned-grid-carrier.json | 32 ++++++++ ...tofit-grid-carries-gap-and-background.json | 35 +++++++++ ...fit-grid-maps-to-minimum-column-width.json | 34 +++++++++ .../parity/html-context-syntax-card-grid.json | 2 +- ...-css-grid-row-stays-transparent-group.json | 2 +- ...explicit-grid-class-non-card-children.json | 6 +- .../html-inline-split-grid-stays-group.json | 2 +- ...svg-class-layout-media-rule-contained.json | 2 +- .../html-resolved-split-grid-stays-group.json | 2 +- .../parity/html-saas-style-boundaries.json | 2 +- .../artifact-author-stylesheet-projection.php | 2 +- .../tests/unit/author-selector-semantics.php | 2 +- 19 files changed, 224 insertions(+), 19 deletions(-) create mode 100644 php-transformer/tests/fixtures/parity/html-asymmetric-grid-css-owned-grid-carrier.json create mode 100644 php-transformer/tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json create mode 100644 php-transformer/tests/fixtures/parity/html-autofit-grid-maps-to-minimum-column-width.json diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index 4e67c8c7..e677d9e7 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -89,7 +89,11 @@ private function normalizeAttrsForBlock(string $name, array $attrs): array } } - if ( in_array($name, array( 'core/buttons', 'core/column', 'core/columns', 'core/group', 'core/heading', 'core/list', 'core/list-item', 'core/paragraph' ), true) ) { + if ( in_array($name, array( 'core/buttons', 'core/column', 'core/columns', 'core/group', 'core/heading', 'core/list', 'core/list-item', 'core/paragraph' ), true) + // A native grid layout renders its gap from blockGap; stripping it + // would substitute the theme default for the source grid gap. + && ! ( 'core/group' === $name && 'grid' === (string) ($attrs['layout']['type'] ?? '') ) + ) { unset($attrs['style']['spacing']['blockGap']); if ( empty($attrs['style']['spacing']) ) { unset($attrs['style']['spacing']); diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 7180c0d3..d80f6f24 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -392,6 +392,24 @@ final class HtmlTransformer private const CSS_OWNED_FLOW_CLASS = 'blocks-engine-css-owned-flow'; + private const CSS_OWNED_GRID_CLASS = 'blocks-engine-css-owned-grid'; + + /** @var list Inline grid declarations carried to the generated stylesheet for css-owned grids. */ + private const CSS_OWNED_GRID_CARRIER_PROPERTIES = array( + 'display', + 'grid-template-columns', + 'grid-template-rows', + 'grid-auto-flow', + 'grid-auto-columns', + 'grid-auto-rows', + 'gap', + 'row-gap', + 'column-gap', + 'align-items', + 'justify-items', + 'place-items', + ); + private const CSS_OWNED_LAYOUT_ITEM_CLASS = 'blocks-engine-css-owned-layout-item'; /** @var array Source control DOM paths mapped to core/button wrapper classes. */ @@ -923,6 +941,11 @@ private function materializeAuthorStylesheet(string $html, string $staticCss, bo // This precedes author CSS so source child margins remain authoritative. $cssParts[] = ':where(.wp-block-group.' . self::CSS_OWNED_FLOW_CLASS . ')>*{margin-block-start:0;margin-block-end:0}'; } + if ( str_contains($serializedBlocks, self::CSS_OWNED_GRID_CLASS) ) { + // Core flow margins are not part of a source grid contract; the + // carried grid geometry (gap) owns the spacing between items. + $cssParts[] = ':where(.wp-block-group.' . self::CSS_OWNED_GRID_CLASS . ')>*{margin-block-start:0;margin-block-end:0}'; + } if ( str_contains($serializedBlocks, self::CSS_OWNED_LAYOUT_ITEM_CLASS) ) { // A semantic Group used as a direct grid/flex item contains native // paragraph blocks. Neutralize only those generated inner defaults. @@ -3925,6 +3948,57 @@ private function authorLayoutBlockFromElement(DOMElement $element, array &$fallb private function cssOwnedGroupAttributes(DOMElement $element): array { $attrs = $this->presentationAttributes($element); + $layout = $attrs['layout'] ?? null; + if ( is_array($layout) && 'grid' === (string) ($layout['type'] ?? '') && '' !== (string) ($layout['minimumColumnWidth'] ?? '') ) { + // The source track list is exactly expressible as native grid + // layout, so WordPress owns the geometry and no css-owned + // demotion is needed. The author gap and container background ride + // on block supports so hairline-divider grids (gap:1px plus a + // background painting through the gaps) survive even without the + // materialized author stylesheet. + $declarations = $this->structuralPresentationDeclarations($element); + $style = is_array($attrs['style'] ?? null) ? $attrs['style'] : array(); + $gap = trim((string) ($declarations['gap'] ?? '')); + if ( 1 === preg_match('/^[0-9]*\.?[0-9]+(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|%)$/i', $gap) && ! isset($style['spacing']['blockGap']) ) { + $style['spacing'] = array_merge(is_array($style['spacing'] ?? null) ? $style['spacing'] : array(), array( 'blockGap' => $gap )); + } + $background = trim((string) ($declarations['background-color'] ?? $declarations['background'] ?? '')); + if ( '' !== $background && ! preg_match('/url\s*\(|gradient\(|[;{}<>]/i', $background) && ! isset($style['color']['background']) ) { + $style['color'] = array_merge(is_array($style['color'] ?? null) ? $style['color'] : array(), array( 'background' => $background )); + } + if ( array() !== $style ) { + $attrs['style'] = $style; + } + + return $attrs; + } + + $display = strtolower(trim((string) ($this->structuralPresentationDeclarations($element)['display'] ?? ''))); + if ( in_array($display, array( 'grid', 'inline-grid' ), true) ) { + // A grid WordPress layout cannot express keeps its geometry under + // CSS ownership: inline grid declarations ride to the generated + // stylesheet on a carrier class, class-owned ones stay retained by + // author stylesheet materialization. The flow demotion would drop + // the tracks and stack the items vertically. + $inlineDeclarations = $this->cssDeclarations($this->attr($element, 'style')); + $carriedProperties = array() === array_intersect_key($inlineDeclarations, array_flip(self::CSS_OWNED_GRID_CARRIER_PROPERTIES)) + ? array() + : self::CSS_OWNED_GRID_CARRIER_PROPERTIES; + $attrs = $this->presentationAttributes($element, array(), $carriedProperties); + unset($attrs['layout']); + $attrs['className'] = $this->mergeClassNames( + (string) ($attrs['className'] ?? ''), + self::CSS_OWNED_LAYOUT_CLASS, + self::CSS_OWNED_GRID_CLASS + ); + $attrs['style'] = array_merge( + is_array($attrs['style'] ?? null) ? $attrs['style'] : array(), + array( 'spacing' => array( 'blockGap' => '0' ) ) + ); + + return $attrs; + } + unset($attrs['layout']); $attrs['className'] = $this->mergeClassNames( (string) ($attrs['className'] ?? ''), diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php index 8bddaa77..33a06e3a 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php @@ -1174,6 +1174,12 @@ private function layoutAttribute(DOMElement $element, string $mergedStyle = ''): return array( 'type' => 'flex' ); } if ( preg_match('/(?:^|;)\s*display\s*:\s*(inline-)?grid\b/', $style) ) { + $minimumColumnWidth = $this->autoRepeatMinimumColumnWidth( + (string) ($mergedDeclarations['grid-template-columns'] ?? $inlineDeclarations['grid-template-columns'] ?? '') + ); + if ( '' !== $minimumColumnWidth ) { + return array( 'type' => 'grid', 'minimumColumnWidth' => $minimumColumnWidth ); + } if ( ! preg_match('/(?:^|;)\s*display\s*:\s*(inline-)?grid\b/', $inlineStyle) && $this->hasOwnStyleHook($element) ) { return array(); } @@ -1237,6 +1243,22 @@ private function layoutFlexWrap(string $value): string return in_array($value, array( 'wrap', 'nowrap' ), true) ? $value : ''; } + /** + * A track list of exactly repeat(auto-fit|auto-fill, minmax(, 1fr)) + * is natively expressible as WordPress grid layout: core renders + * minimumColumnWidth as repeat(auto-fill, minmax(min(, 100%), 1fr)). + * Every other track list (fixed counts, asymmetric tracks, nested + * functions) returns '' and stays under author CSS ownership. + */ + private function autoRepeatMinimumColumnWidth(string $tracks): string + { + if ( 1 === preg_match('/^repeat\(\s*auto-(?:fit|fill)\s*,\s*minmax\(\s*([0-9]*\.?[0-9]+(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|%))\s*,\s*1fr\s*\)\s*\)$/i', trim($tracks), $matches) ) { + return strtolower($matches[1]); + } + + return ''; + } + /** * Unambiguous grid class tokens: a bare `grid`, a numbered `grid-N`, or any * `*-grid` / `*_grid` suffix (footer-grid, card-grid, mission-grid, …) plus diff --git a/php-transformer/tests/contract/wordpress-site-plan.php b/php-transformer/tests/contract/wordpress-site-plan.php index 6342f9c7..4f277be8 100644 --- a/php-transformer/tests/contract/wordpress-site-plan.php +++ b/php-transformer/tests/contract/wordpress-site-plan.php @@ -83,7 +83,7 @@ $authorLayoutPlan = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '
One
Two
Three
Four
Five
')))->toArray()['source_reports']['wordpress_site_plan'] ?? array(); $authorLayoutMarkup = (string) (($authorLayoutPlan['pages'][0]['canonical_block_markup'] ?? '')); $authorLayoutAssets = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), $authorLayoutPlan['assets'] ?? array())); -$assert(str_contains($authorLayoutMarkup, 'wp-block-group ex-row blocks-engine-css-owned-layout blocks-engine-css-owned-flow') && str_contains($authorLayoutMarkup, '
compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '
', 'about.html' => '
About
')))->toArray()['source_reports']['wordpress_site_plan'] ?? array(); $authorLayoutRouteMarkup = (string) ($authorLayoutRoutePlan['pages'][0]['canonical_block_markup'] ?? ''); $assert(str_contains($authorLayoutRouteMarkup, '"url":"/about"') && ! str_contains($authorLayoutRouteMarkup, '"sourceAttributes":{"href"') && ! str_contains($authorLayoutRouteMarkup, 'href="about.html"'), 'Author-layout anchors expose local routes as first-class URLs in canonical site plans.'); diff --git a/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction-base64.json b/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction-base64.json index 55512a7a..40367b80 100644 --- a/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction-base64.json +++ b/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction-base64.json @@ -25,7 +25,9 @@ }, "expect": [ { "path": "status", "assert": "equals", "value": "success" }, - { "path": "assets", "assert": "count", "count": 1 }, + { "path": "assets", "assert": "count", "count": 2 }, + { "path": "assets.1.path", "assert": "equals", "value": "assets/css/source-author-f4582a2f44445ebc.css" }, + { "path": "assets.1.content", "assert": "contains", "value": ":where(.wp-block-group.blocks-engine-css-owned-grid)>*{margin-block-start:0;margin-block-end:0}" }, { "path": "assets.0.path", "assert": "equals", "value": "index.inline.css" }, { "path": "assets.0.kind", "assert": "equals", "value": "css" }, { "path": "assets.0.role", "assert": "equals", "value": "stylesheet" }, diff --git a/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction.json b/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction.json index b665f089..e4035bb9 100644 --- a/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction.json +++ b/php-transformer/tests/fixtures/parity/artifact-inline-style-extraction.json @@ -25,7 +25,9 @@ }, "expect": [ { "path": "status", "assert": "equals", "value": "success" }, - { "path": "assets", "assert": "count", "count": 1 }, + { "path": "assets", "assert": "count", "count": 2 }, + { "path": "assets.1.path", "assert": "equals", "value": "assets/css/source-author-f4582a2f44445ebc.css" }, + { "path": "assets.1.content", "assert": "contains", "value": ":where(.wp-block-group.blocks-engine-css-owned-grid)>*{margin-block-start:0;margin-block-end:0}" }, { "path": "assets.0.path", "assert": "equals", "value": "index.inline.css" }, { "path": "assets.0.kind", "assert": "equals", "value": "css" }, { "path": "assets.0.role", "assert": "equals", "value": "stylesheet" }, diff --git a/php-transformer/tests/fixtures/parity/artifact-linked-css-layout-wrapper-style-signals.json b/php-transformer/tests/fixtures/parity/artifact-linked-css-layout-wrapper-style-signals.json index f89606eb..363ba1dd 100644 --- a/php-transformer/tests/fixtures/parity/artifact-linked-css-layout-wrapper-style-signals.json +++ b/php-transformer/tests/fixtures/parity/artifact-linked-css-layout-wrapper-style-signals.json @@ -34,13 +34,13 @@ { "path": "source_reports.wordpress_site_plan.diagnostics.0.code", "assert": "equals", "value": "author_layout_topology_changed" }, { "path": "serialized_blocks", "assert": "contains", "value": "hero-grid" }, { "path": "blocks.0.innerBlocks.0.blockName", "assert": "equals", "value": "core/group" }, - { "path": "serialized_blocks", "assert": "contains", "value": "" }, - { "path": "serialized_blocks", "assert": "contains", "value": "
" }, + { "path": "serialized_blocks", "assert": "contains", "value": "" }, + { "path": "serialized_blocks", "assert": "contains", "value": "
" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "" }, - { "path": "serialized_blocks", "assert": "contains", "value": "
" }, + { "path": "serialized_blocks", "assert": "contains", "value": "" }, + { "path": "serialized_blocks", "assert": "contains", "value": "
" }, { "path": "blocks.0.innerBlocks.2.blockName", "assert": "equals", "value": "core/group" }, { "path": "serialized_blocks", "assert": "contains", "value": "" }, { "path": "serialized_blocks", "assert": "contains", "value": "
" }, diff --git a/php-transformer/tests/fixtures/parity/html-asymmetric-grid-css-owned-grid-carrier.json b/php-transformer/tests/fixtures/parity/html-asymmetric-grid-css-owned-grid-carrier.json new file mode 100644 index 00000000..72aeea7d --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-asymmetric-grid-css-owned-grid-carrier.json @@ -0,0 +1,32 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-asymmetric-grid-css-owned-grid-carrier", + "description": "An inline-styled grid with asymmetric tracks (260px 1fr) is not expressible as native WordPress grid layout. Instead of the flow demotion that silently drops the grid geometry, the container must carry a blocks-engine-css-owned-grid marker plus a generated stylesheet rule preserving display:grid, the track list, and the gap.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-asymmetric-grid-css-owned-grid-carrier.json", + "notes": "Derived from portfolio sites whose sidebar/content splits use inline display:grid with asymmetric fr tracks; the css-owned flow demotion stacked the panes vertically because no mechanism carried the inline grid declarations into the generated stylesheet." + }, + "legacy_comparison": { + "skip": true, + "reason": "Covers current PHP transformer layout classification behavior; no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "

Selected stills from the field.

Field Notes

Photographs from three seasons of survey work.

" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group" } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks.0.innerBlocks", "assert": "count", "count": 2 }, + { "path": "serialized_blocks", "assert": "contains", "value": "blocks-engine-css-owned-grid" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "blocks-engine-css-owned-flow" }, + { "path": "assets.0.content", "assert": "contains", "value": "display:grid" }, + { "path": "assets.0.content", "assert": "contains", "value": "grid-template-columns:260px 1fr" }, + { "path": "assets.0.content", "assert": "contains", "value": "gap:32px" } + ] +} diff --git a/php-transformer/tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json b/php-transformer/tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json new file mode 100644 index 00000000..99c8c6b8 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json @@ -0,0 +1,35 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-autofit-grid-carries-gap-and-background", + "description": "An auto-fit grid using the hairline-divider technique (gap:1px plus a container background painting through the gaps) must carry both onto the native grid group: the author gap becomes blockGap so WordPress does not substitute its default gap, and the container background becomes a color support so the dividers survive without the author stylesheet.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json", + "notes": "Derived from a portfolio work grid where gap:1px;background:var(--ink) painted hairline separators between cells; mapping the grid to native layout without carrying the gap rendered WordPress's default block gap instead of 1px dividers." + }, + "legacy_comparison": { + "skip": true, + "reason": "Covers current PHP transformer layout classification behavior; no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "

Ledger

Atlas

Relay

" + }, + "expected_blocks": [ + { + "path": "blocks.0", + "name": "core/group", + "attrs": { + "layout": { "type": "grid", "minimumColumnWidth": "240px" } + } + } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": "1px" }, + { "path": "blocks.0.attrs.style.color.background", "assert": "equals", "value": "#1a1a1a" }, + { "path": "serialized_blocks", "assert": "contains", "value": "is-layout-grid" } + ] +} diff --git a/php-transformer/tests/fixtures/parity/html-autofit-grid-maps-to-minimum-column-width.json b/php-transformer/tests/fixtures/parity/html-autofit-grid-maps-to-minimum-column-width.json new file mode 100644 index 00000000..a04ba5a4 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-autofit-grid-maps-to-minimum-column-width.json @@ -0,0 +1,34 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-autofit-grid-maps-to-minimum-column-width", + "description": "An author-CSS grid container using repeat(auto-fit, minmax(W, 1fr)) is exactly expressible as native WordPress grid layout. It must become a core/group with layout {type:grid, minimumColumnWidth:W} instead of demoting to the css-owned flow group, which stacks the cards in a single column and loses the responsive multi-column arrangement.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-autofit-grid-maps-to-minimum-column-width.json", + "notes": "Derived from a portfolio homepage where .work-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)) } collapsed to a vertical stack after transform: the css-owned demotion dropped the layout attribute and the carried CSS never reproduced the auto-fit tracks." + }, + "legacy_comparison": { + "skip": true, + "reason": "Covers current PHP transformer layout classification behavior; no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "

Ledger

Design system for a fintech team.

Atlas

Mapping tools for field research.

Relay

Realtime dashboard for dispatch.

" + }, + "expected_blocks": [ + { + "path": "blocks.0", + "name": "core/group", + "attrs": { "layout": { "type": "grid", "minimumColumnWidth": "240px" } } + } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks.0.innerBlocks", "assert": "count", "count": 3 }, + { "path": "serialized_blocks", "assert": "contains", "value": "is-layout-grid" }, + { "path": "serialized_blocks", "assert": "contains", "value": "\"minimumColumnWidth\":\"240px\"" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "blocks-engine-css-owned-flow" } + ] +} diff --git a/php-transformer/tests/fixtures/parity/html-context-syntax-card-grid.json b/php-transformer/tests/fixtures/parity/html-context-syntax-card-grid.json index 5f0d7627..ac72b30c 100644 --- a/php-transformer/tests/fixtures/parity/html-context-syntax-card-grid.json +++ b/php-transformer/tests/fixtures/parity/html-context-syntax-card-grid.json @@ -40,7 +40,7 @@ { "path": "fallbacks.1.repair_bucket", "assert": "equals", "value": "restore_interactive_behavior" }, { "path": "diagnostics.2.runtime_island_type", "assert": "equals", "value": "unsupported_custom_app_control" }, { "path": "serialized_blocks", "assert": "contains", "value": "const count = 2;" }, - { "path": "serialized_blocks", "assert": "contains", "value": "cards-grid blocks-engine-css-owned-layout" }, + { "path": "serialized_blocks", "assert": "contains", "value": "blocks-engine-css-owned-layout blocks-engine-css-owned-grid" }, { "path": "serialized_blocks", "assert": "contains", "value": "
" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "blocks-engine/author-layout" }, { "path": "source_reports.html.source_provenance.0.context.structure_signals.grid_like", "assert": "equals", "value": true }, diff --git a/php-transformer/tests/fixtures/parity/html-css-grid-row-stays-transparent-group.json b/php-transformer/tests/fixtures/parity/html-css-grid-row-stays-transparent-group.json index 0a31df4c..73f3c0d5 100644 --- a/php-transformer/tests/fixtures/parity/html-css-grid-row-stays-transparent-group.json +++ b/php-transformer/tests/fixtures/parity/html-css-grid-row-stays-transparent-group.json @@ -24,7 +24,7 @@ "expected_fallbacks": [], "expect": [ { "path": "status", "assert": "equals", "value": "success" }, - { "path": "serialized_blocks", "assert": "contains", "value": "event-row blocks-engine-css-owned-layout blocks-engine-css-owned-flow" }, + { "path": "serialized_blocks", "assert": "contains", "value": "blocks-engine-css-owned-layout blocks-engine-css-owned-grid" }, { "path": "serialized_blocks", "assert": "contains", "value": "date blocks-engine-css-owned-layout" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "blocks-engine/author-layout" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "" }, - { "path": "serialized_blocks", "assert": "contains", "value": "
" }, + { "path": "serialized_blocks", "assert": "contains", "value": "" }, + { "path": "serialized_blocks", "assert": "contains", "value": "
" }, { "path": "serialized_blocks", "assert": "contains", "value": "\"className\":\"reveal reveal-delay-1\"" }, { "path": "serialized_blocks", "assert": "contains", "value": "" } ] diff --git a/php-transformer/tests/fixtures/parity/html-inline-split-grid-stays-group.json b/php-transformer/tests/fixtures/parity/html-inline-split-grid-stays-group.json index 64ad8513..d8559282 100644 --- a/php-transformer/tests/fixtures/parity/html-inline-split-grid-stays-group.json +++ b/php-transformer/tests/fixtures/parity/html-inline-split-grid-stays-group.json @@ -23,7 +23,7 @@ "expected_fallbacks": [], "expect": [ { "path": "status", "assert": "equals", "value": "success" }, - { "path": "serialized_blocks", "assert": "contains", "value": "
" }, + { "path": "serialized_blocks", "assert": "contains", "value": "blocks-engine-css-owned-layout blocks-engine-css-owned-grid\">" }, { "path": "serialized_blocks", "assert": "contains", "value": "feature-copy blocks-engine-css-owned-layout" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "blocks-engine/author-layout" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "" }, - { "path": "serialized_blocks", "assert": "contains", "value": "" }, + { "path": "serialized_blocks", "assert": "contains", "value": "" }, { "path": "serialized_blocks", "assert": "contains", "value": "" }, { "path": "fallbacks", "assert": "count", "count": 0 }, { "path": "coverage.0.fallback_count", "assert": "equals", "value": 0 } diff --git a/php-transformer/tests/fixtures/parity/html-head-meta-description-surfaced.json b/php-transformer/tests/fixtures/parity/html-head-meta-description-surfaced.json new file mode 100644 index 00000000..abbee2cc --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-head-meta-description-surfaced.json @@ -0,0 +1,28 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-head-meta-description-surfaced", + "description": "Named head metadata (meta name=description and social property tags) must surface in the transform result instead of being silently stripped, so consumers can consciously carry it to the destination document.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-head-meta-description-surfaced.json", + "notes": "Derived from design-preview pipelines where meta name=description vanished between authored HTML and preview output with no fallback or diagnostic, reading as a malformed design rather than a deliberate omission." + }, + "legacy_comparison": { + "skip": true, + "reason": "Covers current PHP transformer head metadata reporting; no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "Mira Vale

Work

" + }, + "expected_blocks": [], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "source_reports.head_metadata", "assert": "count", "count": 2 }, + { "path": "source_reports.head_metadata.0.name", "assert": "equals", "value": "description" }, + { "path": "source_reports.head_metadata.0.content", "assert": "equals", "value": "Portfolio of Mira Vale, independent creative director." }, + { "path": "source_reports.head_metadata.1.property", "assert": "equals", "value": "og:title" } + ] +} diff --git a/php-transformer/tests/fixtures/parity/html-layout-gated-by-block-supports.json b/php-transformer/tests/fixtures/parity/html-layout-gated-by-block-supports.json new file mode 100644 index 00000000..1139ed8c --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-layout-gated-by-block-supports.json @@ -0,0 +1,33 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-layout-gated-by-block-supports", + "description": "Blocks whose supports do not accept an authorable layout attribute must never carry one. An inline-grid blockquote becomes core/quote and an inline auto-fit grid list becomes core/list; stamping layout {type:grid} on either bakes is-layout-grid classes into save markup the vendored block-library never emits, so downstream re-serialization rejects the block and reverts the whole section.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-layout-gated-by-block-supports.json", + "notes": "Derived from a portfolio homepage where an inline display:grid testimonial blockquote and an inline repeat(auto-fit, minmax(190px, 1fr)) clients list both reverted to unmodified markup downstream. core/quote declares layout {allowEditing:false} (block-managed, never authorable) and core/list declares no layout support at all." + }, + "legacy_comparison": { + "skip": true, + "reason": "Covers current PHP transformer layout classification behavior; no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "

Design is the argument.

Casey Pryor
  • Acme
  • Northwind
  • Initech
" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/quote" }, + { "path": "blocks.1", "name": "core/list" } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks.0.attrs.layout", "assert": "equals", "value": null }, + { "path": "blocks.1.attrs.layout", "assert": "equals", "value": null }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "is-layout-grid" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "\"layout\"" }, + { "path": "serialized_blocks", "assert": "contains", "value": "blocks-engine-css-owned-grid" }, + { "path": "assets.0.content", "assert": "contains", "value": "grid-template-columns:repeat(auto-fit,minmax(190px,1fr))" } + ] +} diff --git a/php-transformer/tests/fixtures/parity/html-picture-gallery-media.json b/php-transformer/tests/fixtures/parity/html-picture-gallery-media.json index b5269538..77f4777b 100644 --- a/php-transformer/tests/fixtures/parity/html-picture-gallery-media.json +++ b/php-transformer/tests/fixtures/parity/html-picture-gallery-media.json @@ -17,7 +17,7 @@ }, "expected_blocks": [ { "path": "blocks.0", "name": "core/image", "attrs": { "className": "hero-picture", "url": "https://example.com/hero.jpg", "alt": "Hero", "srcset": "https://example.com/hero-large.jpg 1200w", "sizes": "100vw" } }, - { "path": "blocks.1", "name": "core/gallery", "attrs": { "className": "gallery-grid", "layout": { "type": "grid" }, "caption": "Gallery caption" } }, + { "path": "blocks.1", "name": "core/gallery", "attrs": { "className": "gallery-grid", "caption": "Gallery caption" } }, { "path": "blocks.1.innerBlocks.0", "name": "core/image", "attrs": { "url": "https://example.com/one.jpg", "alt": "One", "srcset": "https://example.com/one-large.jpg 900w", "caption": "One caption" } }, { "path": "blocks.1.innerBlocks.1", "name": "core/image", "attrs": { "className": "tile is-resized", "url": "https://example.com/two.jpg", "alt": "Two", "width": "400", "height": "300", "caption": "Two caption" } } ], From cc395f9dd694d91afdf314b096aec0dec8d7a348 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Wed, 5 Aug 2026 21:59:52 -0400 Subject: [PATCH 3/4] fix(php-transformer): order carried declarations by source and scope marker ingestion ## Summary Adversarial-review hardening of the grid-carry and layout-gating work: carried declarations emit in source order so the CSS cascade matches the author document, marker classes are filtered at ingestion so round-tripped transformer output cannot trip the grid heuristics, and the block-supports allowlists follow the vendored rule exactly. ## Why The generated carrier rule sorted declarations alphabetically with per-declaration !important, letting the alphabet pick shorthand/longhand winners: a grid shorthand declared after grid-template-columns was reversed, gap reset a later column-gap, and grid-row-gap aliases flipped. display:grid !important defeated the carrier detection entirely, silently dropping list grids. The head-metadata report was unbounded (a hostile document could inflate diagnostics arbitrarily). Re-ingested transformer output preserved blocks-engine-css-owned-grid as an author class, which matched the *-grid heuristic and turned stacked content into an auto-fill grid. The layout allowlist contradicted its own rule by including columns and gallery (both declare allowEditing:false, like quote) and omitting the layout:true blocks (accordion, tabs, term templates). ## How inlineGeometryClassName orders carried declarations by inline source position (non-inline fallbacks sort last); isCssOwnedGridElement strips a trailing !important before comparing display values; headMetadataReport caps output at 20 entries and 500 chars per content value; a new ingestion-only isTransformerMarkerClassName predicate excludes blocks-engine-*/be-inline-geometry-* tokens from preserved classNames and the grid-class heuristics (deliberately not in GeneratedGutenbergClassPolicy, which BlockFactory also uses to filter emitted classNames); the dl-to-list fallback gains the same grid-carrier branch as ul/ol; and the BlockFactory allowlists now match the vendored block.json supports rule exactly. Three test pins that encoded the old alphabetical serialization order were updated to the source-order output. ## Testing - [ ] composer test (269 parity fixtures, all contract and unit suites pass) - [ ] Adversarial repro probes: shorthand-after-longhand and gap-after-column-gap orderings match browser cascade; !important list grids get the carrier; marker-class round-trip produces no layout, no preserved marker, no spurious reset rule; meta report capped at 20x500 --- .../src/HtmlToBlocks/BlockFactory.php | 24 ++++++++---- .../src/HtmlToBlocks/HtmlTransformer.php | 16 ++++++-- .../Style/StyleResolutionTrait.php | 39 +++++++++++++++++-- .../tests/contract/empty-visual-figure.php | 2 +- .../tests/unit/author-selector-semantics.php | 5 ++- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index 4ec2e792..b3a406ce 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -30,12 +30,11 @@ final class BlockFactory * @var array */ private const LAYOUT_SUPPORTING_BLOCKS = array( + 'core/accordion' => true, 'core/buttons' => true, 'core/column' => true, - 'core/columns' => true, 'core/comments-pagination' => true, 'core/cover' => true, - 'core/gallery' => true, 'core/group' => true, 'core/navigation' => true, 'core/post-content' => true, @@ -43,21 +42,30 @@ final class BlockFactory 'core/query' => true, 'core/query-pagination' => true, 'core/social-links' => true, + 'core/tab-list' => true, + 'core/tab-panel' => true, + 'core/term-template' => true, + 'core/terms-query' => true, ); /** * The subset whose supports.layout permits switching to type grid - * (no allowSwitching:false pin to a fixed flex default). + * (layout true, or an object without an allowSwitching:false pin to a + * fixed flex default). * * @var array */ private const GRID_LAYOUT_BLOCKS = array( - 'core/column' => true, - 'core/cover' => true, - 'core/group' => true, - 'core/post-content' => true, + 'core/accordion' => true, + 'core/column' => true, + 'core/cover' => true, + 'core/group' => true, + 'core/post-content' => true, 'core/post-template' => true, - 'core/query' => true, + 'core/query' => true, + 'core/tab-panel' => true, + 'core/term-template' => true, + 'core/terms-query' => true, ); private ?StyleAttributeMapper $styleMapper = null; diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 1fdf7b8e..87a0c26e 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -950,11 +950,11 @@ private function headMetadataReport(string $html): array $entries[] = array_filter(array( 'name' => $name, 'property' => $property, - 'content' => $content, + 'content' => substr($content, 0, 500), ), static fn (string $value): bool => '' !== $value); } - return $entries; + return array_slice($entries, 0, 20); } private function materializeAuthorStylesheet(string $html, string $staticCss, bool $includeAuthorStyles = true, string $serializedBlocks = ''): void @@ -2613,7 +2613,11 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca $items = $this->definitionListItems($element); if ( array() !== $items ) { - return $this->createBlock('core/list', $this->presentationAttributes($element), $items, $element); + $definitionListAttrs = $this->isCssOwnedGridElement($element) + ? $this->cssOwnedGridAttributes($element) + : $this->presentationAttributes($element); + + return $this->createBlock('core/list', $definitionListAttrs, $items, $element); } $children = $this->convertChildren($element, $fallbacks, true); @@ -4078,7 +4082,11 @@ private function cssOwnedGroupAttributes(DOMElement $element): array private function isCssOwnedGridElement(DOMElement $element): bool { - $display = strtolower(trim((string) ($this->structuralPresentationDeclarations($element)['display'] ?? ''))); + $display = strtolower(trim((string) preg_replace( + '/\s*!important\s*$/i', + '', + (string) ($this->structuralPresentationDeclarations($element)['display'] ?? '') + ))); return in_array($display, array( 'grid', 'inline-grid' ), true); } diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php index 462b0c5b..fba3aeed 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php @@ -323,7 +323,13 @@ private function inlineGeometryClassName(DOMElement $element, array $excludedPro return ''; } - ksort($geometry); + // Emit carried declarations in source order: with per-declaration + // !important, last-write-wins is decided by rule order, and an + // alphabetical sort silently flips shorthand/longhand winners + // (grid vs grid-template-columns, gap vs column-gap). Values not + // present inline (forced/custom-property fallbacks) sort last. + $sourceOrder = array_flip(array_keys($declarations)); + uksort($geometry, static fn (string $a, string $b): int => (($sourceOrder[$a] ?? PHP_INT_MAX) <=> ($sourceOrder[$b] ?? PHP_INT_MAX)) ?: strcmp($a, $b)); $declarations = array(); foreach ($geometry as $property => $value) { // A converted inline declaration must continue to outrank authored @@ -1106,11 +1112,24 @@ private function selectorCarriesPseudoState(string $selector): bool private function presentationClassName(string $className): string { $classes = preg_split('/\s+/', trim($className)) ?: array(); - $classes = array_filter($classes, static fn (string $class): bool => '' !== $class && ! self::isBehaviorHookClassName($class) && ! self::isGeneratedCoreClassName($class)); + $classes = array_filter($classes, static fn (string $class): bool => '' !== $class && ! self::isBehaviorHookClassName($class) && ! self::isGeneratedCoreClassName($class) && ! self::isTransformerMarkerClassName($class)); return implode(' ', array_values(array_unique($classes))); } + /** + * Transformer-generated marker and carrier classes found in SOURCE markup + * (re-ingested transformer output) must be re-derived, not preserved as + * author classes: a preserved css-owned-grid marker would trip the + * grid-class heuristics and the carried margin reset. Emitted classNames + * are unaffected — this filters ingestion only. + */ + private static function isTransformerMarkerClassName(string $className): bool + { + return str_starts_with($className, 'blocks-engine-') + || str_starts_with($className, 'be-inline-geometry-'); + } + private static function isBehaviorHookClassName(string $className): bool { return 1 === preg_match('/^js(?:$|[-_:]|[A-Z])/', $className); @@ -1271,13 +1290,25 @@ private function autoRepeatMinimumColumnWidth(string $tracks): string */ private function hasExplicitGridClass(DOMElement $element): bool { - $className = strtolower($this->attr($element, 'class')); + $className = $this->authorClassTokens($element); return (bool) preg_match('/(?:^|[\s_-])(?:grid|grid-[0-9]+|grid-cols(?:-[0-9]+)?|grid-columns|[a-z0-9]+[-_]grid)(?:$|[\s_-])/', $className); } private function hasGridLikeClass(DOMElement $element): bool { - $className = strtolower($this->attr($element, 'class')); + $className = $this->authorClassTokens($element); return (bool) preg_match('/(?:^|[\s_-])(?:cards|features|services|providers|testimonials|resources|posts|projects|stats|badges|grid|grid-[0-9]+|tiles|collection|gallery)(?:$|[\s_-])/', $className); } + + /** + * Class tokens with generated markers filtered out, so transformer-emitted + * classes (blocks-engine-css-owned-grid, …) re-ingested from prior output + * never trip the author grid-class heuristics. + */ + private function authorClassTokens(DOMElement $element): string + { + $tokens = preg_split('/\s+/', strtolower(trim($this->attr($element, 'class')))) ?: array(); + + return implode(' ', array_filter($tokens, static fn (string $token): bool => '' !== $token && ! GeneratedGutenbergClassPolicy::isGeneratedClassName($token) && ! self::isTransformerMarkerClassName($token))); + } } diff --git a/php-transformer/tests/contract/empty-visual-figure.php b/php-transformer/tests/contract/empty-visual-figure.php index db21f60f..3503e1d5 100644 --- a/php-transformer/tests/contract/empty-visual-figure.php +++ b/php-transformer/tests/contract/empty-visual-figure.php @@ -47,7 +47,7 @@ $inlineMarkup = (string) ($inlineCompiled['serialized_blocks'] ?? ''); $inlineValidity = ( new BlockValidityValidator() )->validateBlocks($inlineCompiled['blocks'] ?? array()); $inlineCssAssets = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), $inlineCompiled['assets'] ?? array())); -$assert(2 === substr_count($inlineMarkup, 'wp-block-group photo') && str_contains($inlineMarkup, 'min-height:var(--h)') && ! str_contains($inlineMarkup, '--a:') && str_contains($inlineCssAssets, '--a:#27485f !important;--b:#87d8ff !important;--h:280px !important') && str_contains($inlineCssAssets, '--a:#6f493e !important;--b:#ff8762 !important;--h:390px !important'), 'Artifact compiler carries fixture87 gallery custom properties in generated CSS while core owns the saved style attribute.'); +$assert(2 === substr_count($inlineMarkup, 'wp-block-group photo') && str_contains($inlineMarkup, 'min-height:var(--h)') && ! str_contains($inlineMarkup, '--a:') && str_contains($inlineCssAssets, '--h:280px !important;--a:#27485f !important;--b:#87d8ff !important') && str_contains($inlineCssAssets, '--h:390px !important;--a:#6f493e !important;--b:#ff8762 !important'), 'Artifact compiler carries fixture87 gallery custom properties in generated CSS while core owns the saved style attribute.'); $assert(! str_contains($inlineMarkup, '--tone:') && str_contains($inlineCssAssets, '--tone:#315b74 !important') && 'pass' === ($inlineValidity['status'] ?? ''), 'Fixture87 card custom paint survives in a generated carrier class without diverging from core style serialization.'); $assert(! str_contains($inlineMarkup, 'class="wp-block-group empty'), 'Final native blocks retain the pseudo paint contract while nonvisual empty figures remain pruned.'); diff --git a/php-transformer/tests/unit/author-selector-semantics.php b/php-transformer/tests/unit/author-selector-semantics.php index 0c3ae707..c2760ce5 100644 --- a/php-transformer/tests/unit/author-selector-semantics.php +++ b/php-transformer/tests/unit/author-selector-semantics.php @@ -141,7 +141,7 @@ $iconOnlyButton = $transform('
'); $iconOnlyMarkup = (string) ($iconOnlyButton['serialized_blocks'] ?? ''); $iconOnlyCss = $css($iconOnlyButton); -$assert(str_contains($iconOnlyMarkup, ''); $labeledIconButtonMarkup = (string) ($labeledIconButton['serialized_blocks'] ?? ''); @@ -358,7 +358,8 @@ $assert( str_contains($customPropertyRoundTripMarkup, 'style="border-color:var(--line);border-style:solid;border-width:1px;border-radius:var(--radius);min-height:430px;padding-top:1.2rem;padding-right:1.2rem;padding-bottom:1.2rem;padding-left:1.2rem"') && ! str_contains($customPropertyRoundTripMarkup, '--accent:') - && str_contains($css($customPropertyRoundTrip), '--accent:#d9b86c !important;--tone:#315b74 !important') + && str_contains($css($customPropertyRoundTrip), '--tone:#315b74 !important') + && str_contains($css($customPropertyRoundTrip), '--accent:#d9b86c !important') && 'pass' === ($customPropertyRoundTrip['source_reports']['wp_block_validity']['status'] ?? ''), 'multiple retained custom properties move to generated carrier CSS while supported styles retain a valid core block round trip', $customPropertyRoundTripMarkup From f5c10e0a6018395820f1f4e5a898f57111375188 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Wed, 5 Aug 2026 23:06:02 -0400 Subject: [PATCH 4/4] test(php-transformer): update fixture87 carrier pin to source order ## Summary The WordPress site-plan integration test pinned the fixture87 custom-property carrier rule in alphabetical order; carried declarations now emit in source order, so the pin follows the inline declaration order (--h, --a, --b). ## Why The carried-declaration ordering fix (source order instead of ksort, so the CSS cascade matches the author document) changed the serialized rule layout. This test only runs under a WordPress test environment (REQUIRE_WP_TESTS=1), so the local composer test chain skipped it and CI caught it on the PR. ## Testing - [ ] Byte-exact replacement verified against the actual compiled carrier rule for the same fixture input locally - [ ] CI WordPress site plan integration re-run on the PR --- php-transformer/tests/integration/wordpress-site-plan.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php-transformer/tests/integration/wordpress-site-plan.php b/php-transformer/tests/integration/wordpress-site-plan.php index 494bc7d9..d6c56d62 100644 --- a/php-transformer/tests/integration/wordpress-site-plan.php +++ b/php-transformer/tests/integration/wordpress-site-plan.php @@ -72,7 +72,7 @@ $fixture87 = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '
Card
', 'assets/site.css' => $fixture87Styles)))->toArray(); $fixture87Saved = serialize_blocks(parse_blocks((string) ($fixture87['serialized_blocks'] ?? ''))); $fixture87Css = implode("\n", array_map(static fn(array $asset): string => 'css' === ($asset['kind'] ?? '') ? (string) ($asset['content'] ?? '') : '', $fixture87['assets'] ?? array())); -$assert(!str_contains($fixture87Saved, '--h:') && !str_contains($fixture87Saved, '--tone:') && str_contains($fixture87Saved, 'min-height:var(--h)') && str_contains($fixture87Saved, 'border-color:#d8dee9;border-style:solid;border-width:1px;border-radius:16px;min-height:430px;padding-top:1.2rem;padding-right:1.2rem;padding-bottom:1.2rem;padding-left:1.2rem') && str_contains($fixture87Css, '--a:#27485f !important;--b:#87d8ff !important;--h:280px !important') && str_contains($fixture87Css, '--tone:#315b74 !important'), 'WordPress parse/save retains fixture87 core group support styles while generated carrier CSS preserves gallery and card custom-property paint.'); +$assert(!str_contains($fixture87Saved, '--h:') && !str_contains($fixture87Saved, '--tone:') && str_contains($fixture87Saved, 'min-height:var(--h)') && str_contains($fixture87Saved, 'border-color:#d8dee9;border-style:solid;border-width:1px;border-radius:16px;min-height:430px;padding-top:1.2rem;padding-right:1.2rem;padding-bottom:1.2rem;padding-left:1.2rem') && str_contains($fixture87Css, '--h:280px !important;--a:#27485f !important;--b:#87d8ff !important') && str_contains($fixture87Css, '--tone:#315b74 !important'), 'WordPress parse/save retains fixture87 core group support styles while generated carrier CSS preserves gallery and card custom-property paint.'); $pageDeclarations = array(); foreach ($resolved['pages'] as $page) $pageDeclarations[$page['source_path']] = $page; $pagesBySource = array(); foreach ($resolved['operations'] as $operation) if ('create_page' === $operation['kind']) { $page = $pageDeclarations[$operation['source_path']] ?? null; if (!is_array($page) || ($operation['post_type'] ?? $page['post_type']) !== $page['post_type']) throw new RuntimeException('Create operation lacks an authoritative post type.'); $id = wp_insert_post(array('post_type' => $page['post_type'], 'post_status' => 'publish', 'post_title' => $page['title'], 'post_name' => $operation['slug'], 'post_parent' => 'page' === $page['post_type'] && '' !== $operation['parent_source_path'] ? ($pagesBySource[$operation['parent_source_path']] ?? 0) : 0, 'post_content' => $page['resolved_block_markup']), true); if (is_wp_error($id)) throw new RuntimeException($id->get_error_message()); update_post_meta($id, '_blocks_engine_reconciliation_identity', $page['reconciliation_identity']); $pageIds[$operation['reconciliation_identity']] = $id; $pagesBySource[$operation['source_path']] = $id; } foreach ($resolved['operations'] as $operation) if ('site_reading' === $operation['kind']) { update_option('show_on_front', $operation['show_on_front']); update_option('page_on_front', $pageIds[$operation['front_page_reconciliation_identity']]); }