Skip to content

Map authored borders to native block border supports - #885

Merged
borkweb merged 4 commits into
trunkfrom
fix/native-border-mapping
Aug 17, 2026
Merged

Map authored borders to native block border supports#885
borkweb merged 4 commits into
trunkfrom
fix/native-border-mapping

Conversation

@borkweb

@borkweb borkweb commented Aug 17, 2026

Copy link
Copy Markdown
Member

Problem

Authored border declarations are silently dropped. Confirmed on a real build, amber-ember:

DESIGN:    <blockquote style="margin:0 0 1.6rem;padding-left:1.2rem;
                              border-left:2px solid var(--secondary);font-family:var(--head);...">

GENERATED: <!-- wp:quote {"style":{"typography":{...},"spacing":{"padding":{"left":"1.2rem"},"margin":{...}}}} -->

margin, padding, font-family, font-size, font-weight and letter-spacing all map to native block supports. border-left vanishes, so the "Comfort is a result, never a method" heading loses its left rule.

The gap was half-built rather than missing: the transformer already emitted 235 native border attributes across seven real projects, but every single one was {"radius": ...}. Nothing emitted width, style or color. Authored inline usage across the corpus: border-top 126, border-bottom 47, plus shorthand and per-side longhands.

Fix

Map authored border declarations to native style.border attributes: shorthand, per-side longhands, and the individual border-width / border-style / border-color forms. Existing radius behaviour is preserved exactly.

WordPress applies style.border only if the target block declares border support, so emitting it on a block that lacks support would be an invisible failure. Support is resolved from the block's declared supports rather than a hand-maintained list of block names: Runtime::blockSupportsBorder() queries the live block registry and falls back to a generated resources/wordpress-6.6-core-block-supports.json when WordPress is not loaded, handling both __experimentalBorder and the stabilized border key. Blocks without border support keep their border via the existing carrier rather than emitting an attribute WordPress would ignore.

Verification

  • Test committed before the fix. At the test-only commit the suite fails 1/104, and the failure output shows amber's quote missing border-left from both attributes and CSS.
  • composer test / composer test:unit: 116 assertions, 0 failures (up from 107).
  • Static parity: byte-identical to a freshly-run trunk baseline (md5 matches), baseline run at this branch's exact merge-base. This is the gate that previously failed Expose ordered engine support CSS as its own asset #877 at 23.02%.
  • Zero parity fixtures edited. The 2772-line supports JSON is a new generated resource produced by tools/generate-core-block-supports.php, not a fixture.
  • Border attributes carrying width/style/color across the seven-project corpus: 0 -> 254 (total border attributes 235 -> 341).

Verified end to end in a generated site: amber-ember's left rule is back.

Notes

Release-gated, same as #884 and #883 -> v0.4.20.

Map supported border components into native block attributes and route unsupported components through the existing CSS carrier. Resolve support from live block declarations or the generated WordPress 6.6 registry.
## Summary

Add failing coverage proving that a per-side authored border serializes the all-sides `has-border-color` class, which WordPress turns into a four-sided border.

## Why

The solved-site promotion gate regressed on both fixtures (15-saas 9.69%, 89-static-site-importer-architecture 7.08%) against a 0.00% threshold. WordPress core ships `html :where(.has-border-color){border-style:solid}` in wp-includes/css/dist/block-library/common.css. That is an all-sides rule, so an element that authored only one side gets `border-style: solid` on the other three with no width, which resolves to the initial `medium` value of 3px painted in `currentColor`.

The core style engine attaches `has-border-color` to the uniform `border.color` definition only. The `border.top` / `border.right` / `border.bottom` / `border.left` definitions in `WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA` carry no `classnames` key at all, so core never emits the class for a per-side color.

## How

Three assertions against `StyleAttributeMapper::serialize()` and one against the serialized Group markup: a side-only border must emit no classes while still emitting its own side declarations, a uniform border color must keep `has-border-color`, and a Group whose only authored border is `border-left` must serialize without the class.

## Testing

- [x] `php tests/unit/block-style-support-conversion.php` fails 2 of the 4 new assertions (2 failed, 123 passed); the two that pass show the coverage is not vacuous.
## Summary

Stop emitting the all-sides `has-border-color` class for a per-side border color, and stop serializing the initial value a shorthand substitutes for a component it omits. Both made WordPress paint borders the author never wrote, which is what regressed the solved-site promotion gate.

## Why

The leading hypothesis for the gate failure was double ownership: the carried author stylesheet still painting a border that the native attribute now also paints. That hypothesis is WRONG. There is no double painting — the authored side renders exactly once, with the right colour and width. The regression is entirely on the sides nobody authored.

Confirmed mechanism, and the evidence for it:

1. WordPress core ships `html :where(.has-border-color){border-style:solid}` in `wp-includes/css/dist/block-library/common.css`, under a comment describing it as a default border style for the border block-support panel. It is an all-sides rule.
2. The core style engine attaches that class to the uniform `border.color` definition only. In `WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA`, `border.color` carries `'classnames' => array('has-border-color' => true, ...)`, while `border.top` / `border.right` / `border.bottom` / `border.left` carry no `classnames` key at all. Core never emits the class for a per-side colour; this transformer did.
3. So an element that authored one side got `border-style: solid` on the other three with no width, which resolves to the initial `border-width: medium` — 3px — painted in `currentColor`, growing the box by up to 6px in each axis.

The gate artifacts confirm this directly. Comparing the candidate DOM snapshots from the failing run against the passing trunk run: trunk's candidates contain zero 3px borders on either fixture, while the failing candidates contain them — 14 side-instances across 5 captured elements on `89-static-site-importer-architecture`, 3 on `15-saas`. Neither source page has a single 3px border. Every affected element carries `has-border-color` in its path selector, and its 3px sides are painted in `currentColor` while its one authored side keeps its real 1px colour. A 3px box change on shared containers is what produced the ghosted text and the concentric rings on the rounded card.

Fixing that exposed a second, separate defect in the same feature. `borderComponentCandidate()` substitutes a component's initial value when a shorthand omits it, which is correct for CSS reset semantics but was also being serialized. `parseBorderShorthand()` rejects `transparent` as a usable colour, so `border: 2px solid transparent` produced an authored-looking `border-color: currentColor`. On `9-membership-community` that inline declaration outranked the author's own `.tier-card:hover{border-color:var(--terracotta-light)}`, freezing three cards at `currentColor`. This is the one place where the original double-ownership intuition was right — but the owner it displaced was a state rule, not the base declaration.

## How

`serialize()` no longer adds `has-border-color` for `border.{side}.color`, matching the core style engine. Core's own per-side `html :where([style*=border-left-color]){border-left-style:solid}` rules still apply to the authored side, and the mapper emits `border-{side}-style` explicitly anyway, so the authored border is unaffected.

`borderComponentCandidate()` now reports `declared: false` for a substituted initial value. Such a value still settles precedence between a shorthand and a longhand, and is still emitted when it must cancel a global border this mapper itself emits — the `border:4px solid blue; border-left:red` reset case is unchanged — but it is never serialized as an authored declaration on its own.

## Testing

- [x] `composer test` — exit 0, no failures. Block style support conversion: 129 passed (107 on trunk). Parity: 275 fixtures. Packaging install proof passed.
- [x] `composer test:unit` — exit 0, no failures.
- [x] Static parity over all 87 website fixtures is byte-identical to a freshly-run `trunk` baseline (vendor copied, not symlinked): md5 `64f061da14c7969f39443292ea95f574` on both. Note this was NOT true before this commit — `9-membership-community` scored 0.6580 on trunk and 0.6556 on the branch, with two extra `border-color` findings; the earlier byte-identical claim did not hold.
- [x] Per-fixture `has-border-color` counts on the two solved fixtures are back to the trunk baseline (`15-saas` 16, `89-static-site-importer-architecture` 4) while keeping the new per-side inline border declarations (3 and 11 respectively). A phantom-medium detector — an all-sides border-style signal without a width for every side — reports 0 candidates on trunk and 0 after this fix, against 3 and 11 before it.
- [x] Corpus count of per-side border objects carrying width/style/colour across `fixtures/websites`: 0 on trunk, 417 on this branch. The original defect stays fixed; the amber-ember blockquote assertion still passes.
- [ ] Solved-site promotion gate: cannot run locally — it lives in another repository and needs a WordPress rendering rig. Pushing so CI arbitrates both fixtures against the 0.00% threshold.
@borkweb
borkweb force-pushed the fix/native-border-mapping branch from 44b1425 to f0068eb Compare August 17, 2026 02:12
@borkweb
borkweb merged commit c0d66ea into trunk Aug 17, 2026
4 checks passed
@borkweb
borkweb deleted the fix/native-border-mapping branch August 17, 2026 05:11
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