Map authored borders to native block border supports - #885
Merged
Conversation
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
force-pushed
the
fix/native-border-mapping
branch
from
August 17, 2026 02:12
44b1425 to
f0068eb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Authored border declarations are silently dropped. Confirmed on a real build, amber-ember:
margin,padding,font-family,font-size,font-weightandletter-spacingall map to native block supports.border-leftvanishes, 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 emittedwidth,styleorcolor. Authored inline usage across the corpus:border-top126,border-bottom47, plus shorthand and per-side longhands.Fix
Map authored border declarations to native
style.borderattributes: shorthand, per-side longhands, and the individualborder-width/border-style/border-colorforms. Existingradiusbehaviour is preserved exactly.WordPress applies
style.borderonly 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 generatedresources/wordpress-6.6-core-block-supports.jsonwhen WordPress is not loaded, handling both__experimentalBorderand the stabilizedborderkey. Blocks without border support keep their border via the existing carrier rather than emitting an attribute WordPress would ignore.Verification
border-leftfrom both attributes and CSS.composer test/composer test:unit: 116 assertions, 0 failures (up from 107).trunkbaseline (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%.tools/generate-core-block-supports.php, not a fixture.width/style/coloracross 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.