Skip to content

fix(php-transformer): demote grid-resolved columns to core/group - #813

Merged
borkweb merged 1 commit into
trunkfrom
fix/grid-demotes-to-columns
Aug 5, 2026
Merged

fix(php-transformer): demote grid-resolved columns to core/group#813
borkweb merged 1 commit into
trunkfrom
fix/grid-demotes-to-columns

Conversation

@borkweb

@borkweb borkweb commented Aug 4, 2026

Copy link
Copy Markdown
Member

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. This closes every path into the core/columns + is-layout-grid combo WordPress rejects.

Why

A two-child container like <div class="hero-grid"> 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 block validation, and forced the LLM fallback in downstream consumers.

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

  • New parity fixture html-split-layout-grid-class-demotes-to-group reproduces the bakery-hero case (written first, verified failing on trunk with the exact invalid serialization).
  • composer test: contract + unit + all 262 parity fixtures pass.
  • composer test:migration:examples passes.
  • Corpus sweep (382 fixture files, trunk vs fix): findings clusters byte-identical, zero new fallbacks. Exactly one file changed - 78-terra-ceramics/contact.html, whose contact-grid section was emitting the invalid wp-block-columns is-layout-grid combo on trunk and now serializes as a grid core/group; its <aside> child also regains its semantic tag instead of flattening to a column <div>.

## 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 <div class="hero-grid"> 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
@borkweb
borkweb merged commit cbb3454 into trunk Aug 5, 2026
4 checks passed
@borkweb
borkweb deleted the fix/grid-demotes-to-columns branch August 5, 2026 00:07
borkweb added a commit that referenced this pull request Aug 6, 2026
…ock supports (#815)

* 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)

* fix(php-transformer): gate layout by block supports and harden grid carry

## Summary
Layout attributes now emit only on blocks whose vendored block.json supports accept them, grids on unsupporting blocks (core/list) ride the css-owned grid carrier instead, named head metadata surfaces in source_reports.head_metadata, and the review pass hardened the grid-carry edges from the previous commit.

## Why
Stamping layout {type:grid} on core/quote (layout is block-managed, allowEditing:false) and core/list (no layout support) baked is-layout-grid classes into save markup the canonical block saves never emit, so downstream re-serialization rejected those blocks and reverted whole sections to unmodified markup. Separately, meta name=description was silently stripped with no fallback, diagnostic, or report, reading as a malformed design rather than a deliberate omission. The adversarial review of the grid-preservation commit also surfaced hardening gaps: gap:0 fell back to the theme default gap, a background shorthand could serialize as an invalid color support, forced carrier synthesis could invent a gap overriding explicit axis gaps, carried values containing comment openers could poison the generated stylesheet, and a dead blockGap assignment clobbered mapped spacing supports.

## How
BlockFactory::normalizeAttrsForBlock gains a supports gate derived from the vendored block-library block.json files: layout passes only for the allowlisted blocks and type:grid only where supports permit switching to grid, and gallery fixtures updated since their pinned grid layout was itself invalid (gallery pins a fixed flex default). Grid-declared list elements route through the extracted cssOwnedGridAttributes helper, so the clients-strip ul keeps its auto-fit tracks via the inline geometry carrier without a layout attribute, and the grid margin reset is class-scoped to cover list hosts. HtmlTransformer::headMetadataReport parses the original document head, reports named meta entries (excluding mechanical charset/viewport) under source_reports.head_metadata, and emits an html_head_metadata_not_carried info diagnostic. The hardening pass restricts carried properties to inline-present declarations, widens the carrier property list to grid shorthands, accepts unitless zero gap, whitelists single color tokens for background promotion, defers to author CSS when conditional rules touch the layout or background family, rejects zero minimumColumnWidth, and drops carried values containing comment openers.

## Testing
- [ ] composer test (269 parity fixtures incl. five new: layout gating, head metadata, zero gap, inline-leaf grid items, css-owned grid carrier; all contract and unit suites pass)
- [ ] Real-corpus check: portfolio-new home.html transforms with zero quote/list layout attrs, two native auto-fit grids, the clients list on the css-owned grid carrier, and zero fallbacks

* 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

* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant