Skip to content

Preserve inline display overrides through the geometry carrier - #883

Merged
borkweb merged 2 commits into
trunkfrom
fix/inline-display-carrier
Aug 15, 2026
Merged

Preserve inline display overrides through the geometry carrier#883
borkweb merged 2 commits into
trunkfrom
fix/inline-display-carrier

Conversation

@borkweb

@borkweb borkweb commented Aug 15, 2026

Copy link
Copy Markdown
Member

An inline display declaration is silently dropped, so a style="display:block" written to
neutralize a class-provided grid disappears and the grid applies unopposed.

The defect

StyleResolutionTrait::inlineGeometryProperties() is the allowlist of inline-style declarations
preserved onto a generated be-inline-geometry-* carrier class. It was sizing-only:

width, height, min-width, min-height, max-width, max-height,
aspect-ratio, box-sizing, flex-basis, object-fit, object-position

display is absent, so an inline display has no carrier and is dropped.
HtmlTransformer::CSS_OWNED_GRID_CARRIER_PROPERTIES does include display, but that path only fires
when the element IS a css-owned grid or flex container. It never fires for a display:block whose
entire purpose is to TURN OFF a grid the element inherits from a class rule.

Why it matters

A consumer that generates HTML with reusable classes commonly re-purposes a layout container by
neutralizing it inline:

<div class="hero-inner" style="display:block">

which transformed to:

<div class="wp-block-group alignwide hero-inner"
     style="margin-top:0;margin-right:auto;margin-bottom:0;margin-left:auto">

The class survives, the neutralizing display does not, so .hero-inner{display:grid; align-items:center} applies with nothing opposing it and every child becomes a grid item. In one
real page that single dropped declaration produced four separate visible defects: a pill stretched
to fill its track, an <h2> pushed into grid column 2, a heading taking the wrong track's scale, and
three cards sitting side by side instead of stacked full width.

Occurrences of inline display across seven real generated sites: 52, 46, 10, 6, 2, 0, 0. The two
sites at zero were the only two with no layout complaint.

What changed

display and its layout companions are carried through the existing geometry-carrier mechanism, via
a new inlineLayoutCarrierProperties() list merged into inlineGeometryProperties().

Two things keep it narrow:

  • The flex companions (flex-direction, flex-wrap, align-items, justify-content, gap) ride
    along only when the inline display is flex/inline-flex. A plain display:block carries
    display alone.
  • Layout properties are carried only when the inline declaration actually overrides author layout;
    otherwise they are dropped as before.

Carried layout declarations emit as their own rule at specificity (0,2,0):

:root .be-inline-geometry-<hash>{display:block}

That beats a plain class selector like .hero-inner (0,1,0) so the neutralization wins, and it uses
no !important — an authored !important keeps its normal cascade priority. Pre-existing
geometry properties keep their separate !important rule at (0,1,0) exactly as before.

Verification

  • test:parity275 fixtures passed, 0 failed
  • Full canonical + unit suites — 0 failures
  • static-parity output is byte-identical to trunk (diff clean across all 87 fixtures). This
    change alters no static-site output, which is expected: the conflict it resolves only exists in a
    WordPress layout context. It also confirms the pre-existing fail rows in that matrix are not
    introduced here.
  • No parity fixture JSON was modified.
  • Confirmed end-to-end on a real consumer build: the pill shrink-wraps, the heading returns to the
    left column, and the cards stack full width, matching the source design.

Adds a deliberately RED contract for block, inline-block, and flex overrides of class-owned grid layout. The expected engine-support selector is :root .be-inline-geometry-* at specificity (0,2,0), without !important, while the existing css-owned-grid rule stays byte-frozen.

RED evidence: php tests/unit/inline-display-carrier.php exits 1 with 12 failed and 14 passed assertions.
Carry display:block, display:inline-block, and display:flex only when matched static or conditional author CSS would reassert a different display mode. Flex-direction, flex-wrap, align-items, justify-content, and gap ride the same carrier for flex/inline-flex; grid-template-columns remains on the unchanged css-owned-grid path.

Emit :root .be-inline-geometry-* at specificity (0,2,0) in source=engine-support before author CSS, with no !important, so a plain author class loses while authored !important retains its cascade priority. Existing sizing and forced grid declarations retain their current important rule.

Gates: focused contract 34 assertions with 0 failures; composer test 275 parity fixtures with 0 failures; static-site raster parity remained 0.0000% for both solved fixtures.
@borkweb
borkweb merged commit a2d0728 into trunk Aug 15, 2026
4 checks passed
@borkweb
borkweb deleted the fix/inline-display-carrier branch August 15, 2026 12:53
borkweb added a commit that referenced this pull request Aug 17, 2026
…font-family (#886)

* test(php-transformer): freeze auto-fit grid and css-owned flex contracts

## Summary
Adds two deliberately RED contracts, one per confirmed defect: `repeat(auto-fit, …)` track lists must not be converted to a native WordPress grid layout attribute, and a flex container demoted to a css-owned core/group must keep its authored `display:flex`.

## Why
`StyleResolutionTrait::autoRepeatMinimumColumnWidth()` accepts both `auto-fit` and `auto-fill` and converts both to `layout.minimumColumnWidth`. `wp-includes/block-supports/layout.php` hardcodes `auto-fill` in every branch that renders that attribute, so an authored `auto-fit` grid renders with empty tracks retained and the content crammed into part of the measure.

`cssOwnedGroupAttributes()` drops the native `layout` attribute on demotion. Grids survive because `CSS_OWNED_GRID_CARRIER_PROPERTIES` rides their inline declarations to the generated stylesheet; there is no flex equivalent, so the block is marked `blocks-engine-css-owned-flow` while the declaration that makes it a flex container is dropped and the children stack.

## How
`tests/unit/auto-fit-grid-carrier.php` covers inline and class-owned `auto-fit` track lists plus `auto-fill` controls proving the native conversion is retained, not abandoned. `tests/unit/css-owned-flex-carrier.php` covers inline row and column flex containers plus controls for a container with no authored display and for a class-owned flex declaration the author stylesheet already owns.

## Testing
RED evidence at this commit: `php tests/unit/auto-fit-grid-carrier.php` exits 1 with 6 failed and 5 passed assertions; `php tests/unit/css-owned-flex-carrier.php` exits 1 with 6 failed and 6 passed assertions.

* fix(php-transformer): keep auto-fit grids and demoted flex containers intact

Restricts the native grid-layout conversion to `auto-fill`, and carries the inline flex declarations of a container demoted to a css-owned core/group.

`autoRepeatMinimumColumnWidth()` accepted `auto-fit` and `auto-fill` alike and converted both to `layout.minimumColumnWidth`. `wp-includes/block-supports/layout.php` hardcodes `auto-fill` in every branch that renders that attribute, so an authored `auto-fit` grid rendered with the empty tracks `auto-fit` collapses still retained, squeezing the content into part of the measure. Measured against a design, delivered tracks came back `267 267 267 267` where the design resolved `592 592 0 0`.

`cssOwnedGroupAttributes()` drops the native `layout` attribute on demotion. Grids survive because `CSS_OWNED_GRID_CARRIER_PROPERTIES` rides their inline declarations to the generated stylesheet. There was no flex equivalent, so a container marked `blocks-engine-css-owned-flow` lost the declaration that made it a flex container and its children stacked.

`auto-fit` now returns '' from the conversion and falls through to the same css-owned-grid carrier that already holds every other track list WordPress cannot express, so the author rule stays the single owner of the exact authored track list. `auto-fill` keeps converting natively.

`CSS_OWNED_FLEX_CARRIER_PROPERTIES` plus `isCssOwnedFlexElement()`/`cssOwnedFlexAttributes()` mirror the grid carrier for flex containers. An inline display that overrides class-owned layout is skipped: #883 already carries those properties complete, at the non-important specificity tier, and forcing them would move them to the important tier.

- [ ] `composer test:unit` — 0 failures across 37 scripts, including the two new contracts at 11 and 12 assertions.
- [ ] `composer test` — 0 failures, 275 parity fixtures.

Seven parity fixtures moved, each because it encoded one of the two defects: three auto-fit fixtures asserted the native conversion (one renamed, since `html-autofit-grid-maps-to-minimum-column-width` named the buggy contract), and four flex fixtures asserted output in which the authored `display:flex` had been dropped.

* test(php-transformer): freeze the button font-family carry contract

## Summary
Adds a deliberately RED contract proving an authored `font-family` on a button-mapped rule must reach the core/button block attributes, and that the other carried declarations keep mapping exactly as they do today.

## Why
`ButtonStyleResolver::BUTTON_TYPOGRAPHY` projects `fontSize`, `fontWeight`, `letterSpacing`, `lineHeight` and `textTransform` onto buttons and drops `font-family`. core/button registers a `fontFamily` attribute, which core injects only when the typography fontFamily support is enabled, so this is a dropped declaration rather than an unsupported one.

The drop is not benign. The authored `.btn` class is consumed into block attributes, so the surviving author rule is rewritten behind `:where()` at zero specificity and matches nothing that can win. theme.json's `styles.elements.button.typography.fontFamily` then substitutes the design-direction typeface, which widens the measure enough to wrap hero CTAs onto two lines.

## How
`tests/unit/button-font-family-carry.php` asserts the support exists before relying on it, pins the resolver's complete attribute output so no other carried declaration can drift, covers a rule that authors no font-family, and repeats the end-to-end shape from the report where the class is consumed.

## Testing
RED evidence at this commit: `php tests/unit/button-font-family-carry.php` exits 1 with 5 failed and 4 passed assertions. The passing four include the support check, which must hold before the fix is safe to write.

* fix(php-transformer): carry an authored font-family onto core/button

## Summary
Adds `fontFamily` to the typography subset projected onto core/button, and to the save-markup serializer that renders those attributes.

## Why
`ButtonStyleResolver` carried a button rule's background, text color, border, radius, font-size, weight, letter-spacing and text-transform, but dropped `font-family`. core/button registers a `fontFamily` attribute, which core injects only when the typography fontFamily support is enabled, so the declaration was expressible all along.

Because the authored class is consumed into block attributes, its surviving rule is rewritten behind `:where()` at zero specificity and cannot win. theme.json's `styles.elements.button.typography.fontFamily` therefore substituted the design-direction typeface, widening hero CTAs enough to wrap them onto two lines.

## How
A raw authored family is a custom value rather than a preset slug, so it rides in `style.typography.fontFamily`. `BlockFactory` did not map that attribute either, so the block attribute alone would not have reached the rendered link; the serializer now emits `font-family` inline the way core's style engine does, ahead of the other typography declarations. Inline style outranks theme.json's element styles, so no `!important` bridge is needed and none was added.

## Testing
- [ ] `composer test:unit` — 0 failures across 39 scripts.
- [ ] `composer test` — 0 failures across 51 scripts, 275 parity fixtures, none edited for this defect.
- [ ] `php tests/unit/button-font-family-carry.php` — 9 assertions, up from 4 passing and 5 failing at the RED commit.
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