From 9b8c0fc96401ca1a634ecc0c998e174c08106641 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Tue, 4 Aug 2026 16:58:22 -0400 Subject: [PATCH] fix(php-transformer): demote grid-resolved columns to group ## Summary Adds a coordination guard so a would-be core/columns container whose presentation layout resolves to grid declines columns conversion and demotes to core/group. Closes every path into the core/columns + is-layout-grid combo WordPress rejects. ## Why A two-child container like
trips ColumnsPattern's split-layout heuristic (two panes -> columns) while the layout resolver independently stamps layout:{type:grid} from the grid-ish class name via hasExplicitGridClass. The pattern's style-based grid bail only sees resolved display declarations, not class-name grid signals, so the invalid combo slipped through, failed WP validation, and forced the LLM fallback. ## How ColumnsPattern::match now consults the same presentationAttributes layout resolution that stamps is-layout-grid and returns null when it resolves to grid, so the host transformer routes the element to core/group where grid layout is native. The only other core/columns creation site, namePriceRowBlockFromElement, gets the identical gate. Keying both gates off the one resolver means the block name and layout attribute can never disagree again, regardless of whether the grid signal is a class name, class-resolved display:grid, or anything future. ## Testing - [ ] composer test (contract + unit + parity: 262 fixtures, includes new html-split-layout-grid-class-demotes-to-group fixture) - [ ] composer test:migration:examples --- .../src/HtmlToBlocks/HtmlTransformer.php | 8 +++++ .../HtmlToBlocks/Patterns/ColumnsPattern.php | 11 +++++++ ...it-layout-grid-class-demotes-to-group.json | 32 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 php-transformer/tests/fixtures/parity/html-split-layout-grid-class-demotes-to-group.json diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 13273532..7180c0d3 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -10190,6 +10190,14 @@ private function namePriceRowBlockFromElement(DOMElement $element, array &$fallb return null; } + // core/columns is a flex layout; WordPress rejects it with is-layout-grid. + // Decline when the resolved layout is grid so the container demotes to + // core/group, where grid layout is native. + $layout = $this->presentationAttributes($element)['layout'] ?? null; + if ( is_array($layout) && 'grid' === (string) ($layout['type'] ?? '') ) { + return null; + } + $rowFallbacks = array(); $columns = array(); foreach ( $children as $child ) { diff --git a/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php index 6c0f3ac7..7c120311 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php @@ -51,6 +51,17 @@ public function match( return null; } + // core/columns is a flex layout; WordPress rejects it with is-layout-grid. + // The layout resolver stamps layout:{type:grid} from signals this + // recognizer's style-based grid bail cannot see (grid-ish class names, + // class-resolved display:grid), so a would-be columns container whose + // layout resolves to grid must decline here and demote to core/group, + // where grid layout is native. + $layout = ( $presentationAttributes($element) )['layout'] ?? null; + if ( is_array($layout) && 'grid' === (string) ($layout['type'] ?? '') ) { + return null; + } + $elementChildren = array(); foreach ( $element->childNodes as $child ) { if ( XML_TEXT_NODE === $child->nodeType && '' === trim($child->textContent ?? '') ) { diff --git a/php-transformer/tests/fixtures/parity/html-split-layout-grid-class-demotes-to-group.json b/php-transformer/tests/fixtures/parity/html-split-layout-grid-class-demotes-to-group.json new file mode 100644 index 00000000..694c3c27 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-split-layout-grid-class-demotes-to-group.json @@ -0,0 +1,32 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-split-layout-grid-class-demotes-to-group", + "description": "A two-child container whose class name trips both the split-layout columns heuristic (hero-grid) and the explicit grid class signal must become a core/group with grid layout, never core/columns. core/columns is a flex layout: pairing it with layout:{type:grid} serializes the wp-block-columns + is-layout-grid combo WordPress rejects.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-split-layout-grid-class-demotes-to-group.json", + "notes": "Derived from a bakery hero where .hero-grid { display:grid } held exactly two panes (copy + plate figure); the split-layout heuristic claimed it as columns while the grid class name stamped is-layout-grid, producing an invalid block." + }, + "legacy_comparison": { + "skip": true, + "reason": "This upstream primitive fixture has no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "

Fresh Bread Daily

Baked every morning.

\"Bread\"/
" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group", "attrs": { "className": "hero-grid", "layout": { "type": "grid" } } } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks", "assert": "count", "count": 1 }, + { "path": "blocks.0.innerBlocks", "assert": "count", "count": 2 }, + { "path": "serialized_blocks", "assert": "contains", "value": "
" }, + { "path": "serialized_blocks", "assert": "contains", "value": "" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "wp:columns" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "wp-block-columns" } + ] +}