Skip to content

Commit 59f8064

Browse files
authored
Preserve native Group grid validity (#839)
Move grid gaps from invalid core/group saved attributes into generated geometry-carrier CSS while retaining class-owned source CSS.\n\nAI assistance: openai/gpt-5.6-sol via OpenCode was used to diagnose screenshot/editor evidence, implement the generic fix, and verify parity. Chris Huber reviewed and remains responsible for the change.
2 parents fe1455a + 8d8d809 commit 59f8064

7 files changed

Lines changed: 40 additions & 27 deletions

php-transformer/src/HtmlToBlocks/BlockFactory.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,7 @@ private function normalizeAttrsForBlock(string $name, array $attrs): array
167167
}
168168
}
169169

170-
if ( in_array($name, array( 'core/buttons', 'core/column', 'core/columns', 'core/group', 'core/heading', 'core/list', 'core/list-item', 'core/media-text', 'core/paragraph' ), true)
171-
// A native grid layout renders its gap from blockGap; stripping it
172-
// would substitute the theme default for the source grid gap.
173-
&& ! ( 'core/group' === $name && 'grid' === (string) ($attrs['layout']['type'] ?? '') )
174-
) {
170+
if ( in_array($name, array( 'core/buttons', 'core/column', 'core/columns', 'core/group', 'core/heading', 'core/list', 'core/list-item', 'core/media-text', 'core/paragraph' ), true) ) {
175171
unset($attrs['style']['spacing']['blockGap']);
176172
if ( empty($attrs['style']['spacing']) ) {
177173
unset($attrs['style']['spacing']);

php-transformer/src/HtmlToBlocks/HtmlTransformer.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3462,6 +3462,15 @@ private function createBlock(string $name, array $attrs = array(), array $innerB
34623462
self::CSS_OWNED_LAYOUT_ITEM_CLASS
34633463
);
34643464
}
3465+
if ( 'core/group' === $name && 'grid' === (string) ($attrs['layout']['type'] ?? '') ) {
3466+
// Core Group's save() does not reproduce a blockGap declaration.
3467+
// Preserve an authored inline gap in a generated carrier instead
3468+
// of storing markup that the editor will mark invalid.
3469+
$gapCarrier = $this->inlineGeometryClassName($sourceElement, array(), array( 'gap' ));
3470+
if ( '' !== $gapCarrier ) {
3471+
$attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $gapCarrier);
3472+
}
3473+
}
34653474
if ( 'core/table' === $name && isset($this->sourceTableMarkers[$this->sourceElementIdentity($sourceElement)]) ) {
34663475
$attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $this->sourceTableMarkers[$this->sourceElementIdentity($sourceElement)]);
34673476
}
@@ -4163,19 +4172,14 @@ private function cssOwnedGroupAttributes(DOMElement $element): array
41634172
$layout = $attrs['layout'] ?? null;
41644173
if ( is_array($layout) && 'grid' === (string) ($layout['type'] ?? '') && '' !== (string) ($layout['minimumColumnWidth'] ?? '') ) {
41654174
// The source track list is exactly expressible as native grid
4166-
// layout, so WordPress owns the geometry and no css-owned
4167-
// demotion is needed. The author gap and container background ride
4168-
// on block supports so hairline-divider grids (gap:1px plus a
4169-
// background painting through the gaps) survive even without the
4170-
// materialized author stylesheet.
4175+
// layout, so WordPress owns the track geometry. Group save markup
4176+
// does not serialize blockGap, so source gap remains stylesheet
4177+
// owned by the normalization in createBlock().
41714178
$declarations = $this->structuralPresentationDeclarations($element);
41724179
$style = is_array($attrs['style'] ?? null) ? $attrs['style'] : array();
4173-
$gap = trim((string) ($declarations['gap'] ?? ''));
4174-
if ( 1 === preg_match('/^(?:0|[0-9]*\.?[0-9]+(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|%))$/i', $gap)
4175-
&& ! isset($style['spacing']['blockGap'])
4176-
&& ! $this->hasConditionalStyleFamily($element, 'layout')
4177-
) {
4178-
$style['spacing'] = array_merge(is_array($style['spacing'] ?? null) ? $style['spacing'] : array(), array( 'blockGap' => $gap ));
4180+
unset($style['spacing']['blockGap']);
4181+
if ( empty($style['spacing']) ) {
4182+
unset($style['spacing']);
41794183
}
41804184
$background = trim((string) ($declarations['background-color'] ?? $declarations['background'] ?? ''));
41814185
if ( 1 === preg_match('/^(#[0-9a-f]{3,8}|[a-z][a-z-]*|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|var)\([^()]*\))$/i', $background)

php-transformer/tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
33
"name": "html-autofit-grid-carries-gap-and-background",
4-
"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.",
4+
"description": "An auto-fit grid using the hairline-divider technique (gap:1px plus a container background painting through the gaps) keeps its native grid tracks while source CSS owns its gap: core/group save markup cannot serialize blockGap. The container background remains a color support so the dividers survive without the author stylesheet.",
55
"source_reference": {
66
"repo": "php-transformer",
77
"path": "tests/fixtures/parity/html-autofit-grid-carries-gap-and-background.json",
8-
"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."
8+
"notes": "Derived from a portfolio work grid where gap:1px;background:var(--ink) painted hairline separators between cells. The source stylesheet retains the gap because core/group does not serialize blockGap in canonical save markup."
99
},
1010
"legacy_comparison": {
1111
"skip": true,
@@ -28,7 +28,7 @@
2828
"expect": [
2929
{ "path": "status", "assert": "equals", "value": "success" },
3030
{ "path": "fallbacks", "assert": "count", "count": 0 },
31-
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": "1px" },
31+
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": null },
3232
{ "path": "blocks.0.attrs.style.color.background", "assert": "equals", "value": "#1a1a1a" },
3333
{ "path": "serialized_blocks", "assert": "contains", "value": "is-layout-grid" }
3434
]

php-transformer/tests/fixtures/parity/html-autofit-grid-carries-zero-gap.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
33
"name": "html-autofit-grid-carries-zero-gap",
4-
"description": "An auto-fit grid with an explicit unitless gap:0 must carry blockGap '0' onto the native grid group; otherwise WordPress substitutes the theme's default block gap for a grid the author declared gapless.",
4+
"description": "An auto-fit grid with an explicit unitless gap:0 keeps native grid tracks without adding blockGap to core/group markup; the source stylesheet remains the gap authority.",
55
"source_reference": {
66
"repo": "php-transformer",
77
"path": "tests/fixtures/parity/html-autofit-grid-carries-zero-gap.json",
8-
"notes": "Companion to html-autofit-grid-carries-gap-and-background: the unitless-zero form of the gap declaration failed the original number+unit pattern, so gapless card walls rendered with the theme default gap."
8+
"notes": "Companion to html-autofit-grid-carries-gap-and-background: source CSS retains the unitless-zero gap because core/group canonical save markup omits blockGap."
99
},
1010
"legacy_comparison": {
1111
"skip": true,
@@ -26,6 +26,6 @@
2626
"expect": [
2727
{ "path": "status", "assert": "equals", "value": "success" },
2828
{ "path": "fallbacks", "assert": "count", "count": 0 },
29-
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": "0" }
29+
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": null }
3030
]
3131
}

php-transformer/tests/fixtures/parity/html-autofit-grid-inline-leaf-items.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
33
"name": "html-autofit-grid-inline-leaf-items",
4-
"description": "An expressible auto-fit grid whose direct children are standalone inline text leaves keeps native grid layout while each leaf rides its own display:contents carrier paragraph, so the item count matches the source children and the spans themselves become the grid items. Locks the standalone-inline-leaf routing branch, which bypasses cssOwnedGroupAttributes.",
4+
"description": "An expressible auto-fit grid whose direct children are standalone inline text leaves keeps native grid layout while source CSS owns its gap. Each leaf rides its own display:contents carrier paragraph, so the item count matches the source children and the spans themselves become the grid items. Locks the standalone-inline-leaf routing branch, which bypasses cssOwnedGroupAttributes.",
55
"source_reference": {
66
"repo": "php-transformer",
77
"path": "tests/fixtures/parity/html-autofit-grid-inline-leaf-items.json",
@@ -29,7 +29,7 @@
2929
{ "path": "status", "assert": "equals", "value": "success" },
3030
{ "path": "fallbacks", "assert": "count", "count": 0 },
3131
{ "path": "blocks.0.innerBlocks", "assert": "count", "count": 4 },
32-
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": "24px" },
32+
{ "path": "blocks.0.attrs.style.spacing.blockGap", "assert": "equals", "value": null },
3333
{ "path": "serialized_blocks", "assert": "contains", "value": "is-layout-grid" },
3434
{ "path": "serialized_blocks", "assert": "contains", "value": "<p class=\"blocks-engine-inline-layout-carrier\"><span class=\"client\">Acme Corp</span></p>" }
3535
]

php-transformer/tests/fixtures/parity/html-explicit-grid-class-non-card-children.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"expected_blocks": [
1919
{ "path": "blocks.0", "name": "core/group", "attrs": { "className": "section" } },
20-
{ "path": "blocks.0.innerBlocks.0", "name": "core/group", "attrs": { "className": "grid-3", "style": { "spacing": { "blockGap": "1.5rem" } }, "layout": { "type": "grid" } } },
20+
{ "path": "blocks.0.innerBlocks.0", "name": "core/group", "attrs": { "layout": { "type": "grid" } } },
2121
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0", "name": "core/group", "attrs": { "className": "reveal reveal-delay-1" } },
2222
{ "path": "blocks.0.innerBlocks.0.innerBlocks.1", "name": "core/group", "attrs": { "className": "reveal reveal-delay-2" } },
2323
{ "path": "blocks.0.innerBlocks.0.innerBlocks.2", "name": "core/group", "attrs": { "className": "reveal reveal-delay-3" } },
@@ -29,8 +29,9 @@
2929
{ "path": "fallbacks", "assert": "count", "count": 0 },
3030
{ "path": "blocks", "assert": "count", "count": 1 },
3131
{ "path": "blocks.0.innerBlocks.0.innerBlocks", "assert": "count", "count": 3 },
32-
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:group {\"className\":\"grid-3\",\"style\":{\"spacing\":{\"blockGap\":\"1.5rem\"}},\"layout\":{\"type\":\"grid\"}} -->" },
33-
{ "path": "serialized_blocks", "assert": "contains", "value": "<div class=\"wp-block-group is-layout-grid wp-block-group-is-layout-grid grid-3\" style=\"gap:1.5rem\">" },
32+
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:group {\"className\":\"grid-3 be-inline-geometry-" },
33+
{ "path": "serialized_blocks", "assert": "contains", "value": "\",\"layout\":{\"type\":\"grid\"}} -->" },
34+
{ "path": "serialized_blocks", "assert": "contains", "value": "<div class=\"wp-block-group is-layout-grid wp-block-group-is-layout-grid grid-3 be-inline-geometry-" },
3435
{ "path": "serialized_blocks", "assert": "contains", "value": "\"className\":\"reveal reveal-delay-1\"" },
3536
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:heading {\"content\":\"You Call, We Plan\",\"level\":4} -->" }
3637
]

php-transformer/tests/unit/block-style-support-conversion.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@
7878
$assert(! isset($groupAttrs['style']['spacing']['blockGap']), '15: core group save omits block gap without a core layout attribute', json_encode($groupAttrs));
7979
$assert(str_contains($groupInnerHtml, 'min-height:100svh'), '16: core group retains supported dimensions', $groupInnerHtml);
8080

81+
$nativeGridHtml = '<div class="tpl-grid" style="display:grid;grid-template-columns:repeat(auto-fit, minmax(290px, 1fr));gap:1.2rem"><p>Fallback card</p></div>';
82+
$nativeGridResult = ( new HtmlTransformer() )->transform($nativeGridHtml, array())->toArray();
83+
$nativeGrid = $nativeGridResult['blocks'][0] ?? array();
84+
$nativeGridAttrs = is_array($nativeGrid['attrs'] ?? null) ? $nativeGrid['attrs'] : array();
85+
$nativeGridMarkup = (string) ($nativeGridResult['serialized_blocks'] ?? '');
86+
$nativeGridCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), is_array($nativeGridResult['assets'] ?? null) ? $nativeGridResult['assets'] : array()));
87+
88+
$assert('grid' === ($nativeGridAttrs['layout']['type'] ?? ''), '16a: representative tpl-grid shape retains native Group grid layout', json_encode($nativeGridAttrs));
89+
$assert(! isset($nativeGridAttrs['style']['spacing']['blockGap']), '16b: native Group grids do not emit noncanonical blockGap attributes', json_encode($nativeGridAttrs));
90+
$assert(! str_contains($nativeGridMarkup, 'gap:1.2rem'), '16c: native Group grid markup omits inline gap that Gutenberg save does not reproduce', $nativeGridMarkup);
91+
$assert(str_contains($nativeGridCss, 'gap:1.2rem !important'), '16d: inline native Group grid gap moves to the generated geometry carrier', $nativeGridCss);
92+
8193
$cardHtml = '<section class="pricing-shell" style="max-width:1120px;margin:0 auto;padding:5rem 2rem"><article class="pricing-card" style="max-width:360px;padding:2rem;background:#fff"><h2>Team</h2><p>Scale every launch.</p></article></section>';
8294
$cardResult = ( new HtmlTransformer() )->transform($cardHtml, array())->toArray();
8395
$cardShell = $cardResult['blocks'][0] ?? array();

0 commit comments

Comments
 (0)