Repository navigation
feat(renderer-blade): compile WP block-supports into wrapper attributes - #457
Conversation
Closes Keystone #50. The front-end renderer's block partials only emitted `tagName`, `layout.type`, and `className` on each wrapper element. Block-support attributes — `align`, `backgroundColor`, `textColor`, `style.color.*`, `style.spacing.*`, `style.border.*`, `style.typography.*`, `fontSize`, `fontFamily`, `borderColor`, `anchor`, etc. — got persisted by the editor but never reached the rendered HTML. The result was a closed authoring loop: change a block's background in the editor → save → front-end shows the un-customized version because no class / inline style carried the change. This commit ports WordPress core's server-side `apply_block_supports()` behavior into a reusable `BlockSupports` helper, then updates every block partial to use it. ## `BlockSupports::compile` Walks the attribute tree and returns `['classes' => string[], 'style' => string, 'id' => ?string]`. Coverage parity with WP core's block-supports modules: - `align` → `align{wide|full|left|center|right}` class. The left / center / right values also emit `has-text-align-{value}` so text blocks (paragraph, heading) and container blocks share the same attribute path. - `textAlign` → `has-text-align-{left|center|right|justify}` class (not inline `text-align: …` — matches WP). - `color`: palette slugs (`backgroundColor`, `textColor`, `gradient`) → `has-{slug}-*` classes plus the `has-background` / `has-text-color` markers. Custom values under `style.color.*` → inline declarations + the same marker classes. Slug wins over custom when both are set (mirrors WP serialization priority). - `spacing`: `style.spacing.padding` / `style.spacing.margin` as a single string (all sides) or per-side object; `blockGap` as `--wp--style--block-gap` custom property or per-axis row-gap / column-gap. - `border`: radius (string or per-corner object), per-side / root color, style, width. Auto-adds `border-style: solid` when width or per-side value is set without an explicit style — same fallback WP serializes. - `typography`: palette slugs (`fontSize` → `has-{slug}-font-size` + `has-defined-font-size`, `fontFamily` → `has-{slug}-font-family`); custom values for `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `lineHeight`, `letterSpacing`, `textTransform`, `textDecoration`. - `className` → user-supplied class string appended verbatim (whitespace-tokenized; deduped at the end). - `anchor` → sanitized `id` attribute. - `var:preset|{taxonomy}|{slug}` references in `style.*` strings expand to `var(--wp--preset--{kebab-path})`. A `wrapperAttrs(attributes, baseClasses)` helper returns the ready-to-splice attribute string each partial uses: <{{ $tag }}{!! BlockSupports::wrapperAttrs( $attributes, [ 'wp-block-group' ] ) !!}> ## Partials updated Every core/* and artisanpack/* partial now delegates wrapper-attribute compilation to `BlockSupports`. Block-specific quirks (e.g. `core/list`'s `start` / `reversed` attributes, `core/spacer`'s `height`, `core/cover`'s `minHeight`, `core/button`'s wrapper-vs-link split) stay in the partial and merge with the compiled output where needed. `core/button` specifically: the visual block-supports (color, border, spacing, typography) land on the inner `<a>` / `<span>` so that `theme.json`'s `styles.elements.button` targeting `.wp-element-button` actually picks up the slug-based class set. The outer wrapper carries only `align` / `className` / `anchor`. Mirrors WP core. ## Tests - 39 new unit tests on `BlockSupports::compile` and `wrapperAttrs` covering each block-supports category, palette-vs-custom priority, preset-reference expansion, slug normalization, anchor sanitization, and the legacy `style: "default"` (core/separator) defensive case. - 7 new integration tests on `BlockRenderer` proving the rendered HTML output now carries the canonical class / style / id markers for custom backgrounds, palette slugs, alignment, padding, typography, anchor, and the `core/button` wrapper-vs-link split. - All 555 existing renderer-blade PHP tests continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds BlockSupports (compile + wrapperAttrs), migrates Blade block templates to use it for wrapper attributes or compiled styles/ids, extends ThemeJsonTokensCompiler to emit layout custom properties and alignment rules, and adds tests covering compile/wrapperAttrs and renderer output. ChangesBlock Supports Compiler & Template Refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php`:
- Around line 4-10: $justify is taken directly from
$attributes['layout']['justifyContent'] and interpolated into the class token
(used in $baseClasses), so validate and normalise it first: trim and cast to
string, then check against an allowlist of permitted tokens (e.g.,
'left','center','right','space-between','space-around','space-evenly' or your
theme's allowed values) and only use the value if it exists in that allowlist;
otherwise fall back to the safe default ('left' or the existing fallback).
Update the logic that sets $justify and the $baseClasses entry to use the
validated token instead of the raw input.
In `@packages/visual-editor-renderer-blade/src/Support/BlockSupports.php`:
- Around line 451-489: The code currently adds inline typography styles from
style.typography even when slug-based attributes exist, so update the loop in
BlockSupports (where $mappings is iterated) to skip generating inline styles for
'fontSize' and 'fontFamily' when the corresponding slug variables
($fontSizeSlug, $fontFamilySlug) are non-empty; i.e., before computing $value
for an $attrKey, if $attrKey is 'fontSize' and $fontSizeSlug !== '' (or if
$attrKey is 'fontFamily' and $fontFamilySlug !== ''), continue the loop so the
slug-based class (created with self::slugify and stringAttr) wins over the
custom inline style produced via self::expandPresetReference.
In `@packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php`:
- Around line 583-597: The test verifies the inner <a> has the color class but
doesn't assert the outer wrapper lacks it; update the test in BlockRendererTest
(the it(...) block using makeBlock, $tree and makeRenderer()->render) to add a
negative assertion that the wrapper div with class "wp-block-button" does NOT
contain "has-accent-background-color" (use the same expect($html) pattern style,
e.g. expect($html)->not->toMatch with a regex targeting the outer div wrapper),
so the contract "color on the inner link, not the outer wrapper" is enforced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 43d1f5cf-b45c-4471-87f1-6981b6e67dda
📒 Files selected for processing (47)
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/audio.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/block.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/code.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/details.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/embed.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/gallery.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/group.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/heading.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/list-item.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/list.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/navigation-link.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/navigation-submenu.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/navigation.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/paragraph.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-author.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-content.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-date.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-excerpt.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-featured-image.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/post-title.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/preformatted.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/pullquote.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/quote.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/row.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/separator.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/site-logo.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/site-tagline.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/site-title.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/spacer.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/stack.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/template-part.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/verse.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/video.blade.phppackages/visual-editor-renderer-blade/src/Support/BlockSupports.phppackages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.phppackages/visual-editor-renderer-blade/tests/Unit/BlockSupportsTest.php
…ffect at the page root The block partials emit `alignwide` / `alignfull` classes on each block now (per the parent commit), but no CSS rule converts those classes into actual widths when the block sits at the page root. WordPress-FSE wraps post content in `.wp-block-post-content` so that `.is-layout-constrained > .alignwide` matches; our renderer output lands directly inside the consumer's `<main>` / template wrapper with no such parent marker, so the alignment classes ride on the section but no CSS sizes it. `ThemeJsonTokensCompiler` now emits two additional pieces alongside the `--wp--preset--*` palette tokens it already produces: - `--wp--style--global--content-size` and `--wp--style--global--wide-size` custom properties pulled from `theme.json` `settings.layout`. Matches WP's own variable names. - Layout rules targeting `.wp-block-group.is-layout-constrained.alignwide` → `max-width: var(--wp--style--global--wide-size); margin: auto` and `.wp-block-group.is-layout-constrained.alignfull` → `max-width: none`. The rules target the constrained group's own classes, not a parent-relative selector, so they apply whether the group sits at the page root or inside another container — which is what the user's symptom needed. The default case (constrained group without an alignment override) is left alone so themes that already style children-of-constrained directly aren't double-constrained. `alignfull` is emitted whenever ANY layout key is configured, since full-bleed is the same regardless of `wideSize` and authors can toggle it on without the renderer caring whether the theme happened to declare `wideSize`. Updated the BlocksStylesComponent "no recognized tokens" test to use `settings.border` instead of `settings.layout`, since layout is now a recognized category. Tests: 5 new unit tests on `ThemeJsonTokensCompiler` covering the custom properties, the alignwide rule, the alignfull rule, the no-layout-omission case, and the root-presets-plus-layout-rules composition. 560/560 PHP green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment respects content-size Continuing on Keystone #50. The prior commit made `alignwide` / `alignfull` work on constrained groups at the page root, but a section with `align="none"` (the default) still stretched full- width — there was no rule giving "unaligned at root" a content- size constraint. In WP-FSE this is handled by wrapping page content in `<main class="wp-block-post-content is-layout-constrained">` so that `.is-layout-constrained > :not(.alignwide):not(.alignfull)` matches the default case. Mirror that in the renderer: `ThemeJsonTokensCompiler` now emits three additional rules scoped to a `.wp-block-post-content.is-layout-constrained` container: - `> :where(:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright))` → `max-width: var(--wp--style--global--content-size); margin: auto` - `> .alignwide` → `max-width: var(--wp--style--global--wide-size); margin: auto` - `> .alignfull` → `max-width: none` The container class is opt-in. Themes add `class="wp-block-post-content is-layout-constrained"` to whichever wrapper element holds their page content (typically `<main>`) and the children get the canonical alignment behavior. Themes that don't add it keep the old "container is full-bleed unless aligned" behavior — the opt-in avoids clashing with header / footer wrappers that are intentionally full-width. The block-level rules (rule set A — `.wp-block-group.is-layout-constrained.alignwide` etc.) continue to ship alongside the post-content rules (rule set B) so authors can pick whichever pattern fits their template. Tests: 1 new unit test pinning the three post-content child rules. 561/561 PHP green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/visual-editor-renderer-blade/tests/Unit/ThemeJsonTokensCompilerTest.php (1)
160-163: ⚡ Quick winStrengthen child-rule assertions to verify behavior, not just selector presence.
This test can still pass if selectors exist but declarations regress. Assert key declarations (
max-width/margin) for the.wp-block-post-contentchild rules too.Suggested test hardening
expect( $css ) ->toContain( '.wp-block-post-content.is-layout-constrained > :where(:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright))' ) ->toContain( '.wp-block-post-content.is-layout-constrained > .alignwide' ) - ->toContain( '.wp-block-post-content.is-layout-constrained > .alignfull' ); + ->toContain( '.wp-block-post-content.is-layout-constrained > .alignfull' ) + ->toContain( 'max-width: var(--wp--style--global--content-size);' ) + ->toContain( 'max-width: var(--wp--style--global--wide-size);' ) + ->toContain( 'max-width: none;' ) + ->toContain( 'margin-left: auto;' ) + ->toContain( 'margin-right: auto;' );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/visual-editor-renderer-blade/tests/Unit/ThemeJsonTokensCompilerTest.php` around lines 160 - 163, The current assertions only check selector presence in ThemeJsonTokensCompilerTest (variable $css) but not the declarations; update the test to assert that the child-rule selectors for '.wp-block-post-content.is-layout-constrained > :where(:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright))', '.wp-block-post-content.is-layout-constrained > .alignwide' and '.wp-block-post-content.is-layout-constrained > .alignfull' include the expected declarations (e.g. contain 'max-width' and the appropriate 'margin' rules) by adding assertions against $css that look for those declaration strings alongside the selector checks so the test verifies behavior, not just selector presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@packages/visual-editor-renderer-blade/tests/Unit/ThemeJsonTokensCompilerTest.php`:
- Around line 160-163: The current assertions only check selector presence in
ThemeJsonTokensCompilerTest (variable $css) but not the declarations; update the
test to assert that the child-rule selectors for
'.wp-block-post-content.is-layout-constrained >
:where(:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright))',
'.wp-block-post-content.is-layout-constrained > .alignwide' and
'.wp-block-post-content.is-layout-constrained > .alignfull' include the expected
declarations (e.g. contain 'max-width' and the appropriate 'margin' rules) by
adding assertions against $css that look for those declaration strings alongside
the selector checks so the test verifies behavior, not just selector presence.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2f21a762-4528-4a1c-805d-4a537c4cc53e
📒 Files selected for processing (2)
packages/visual-editor-renderer-blade/src/Services/ThemeJsonTokensCompiler.phppackages/visual-editor-renderer-blade/tests/Unit/ThemeJsonTokensCompilerTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/visual-editor-renderer-blade/src/Services/ThemeJsonTokensCompiler.php
Three findings:
- `core/buttons` partial — `layout.justifyContent` was concatenated
into a class token without validation. Whitelist against WP's
enum (`left`, `center`, `right`, `space-between`, `space-around`,
`space-evenly`, `stretch`); anything outside falls back to `left`
(WP's own default). Closes the path that let an authored block
smuggle arbitrary class tokens through the stored attribute.
- `BlockSupports::applyTypography` — slug-over-custom precedence
for `fontSize` / `fontFamily`. The class path already emitted
`has-{slug}-font-size` from the slug attribute; the loop was
still emitting the inline `font-size: …` declaration from
`style.typography.fontSize` when both were set. Inline style
would win the cascade and override the slug class, contradicting
the documented priority that mirrors the color path's behavior.
Skip the inline declaration when the slug is present.
- `core/button` renderer test — added the missing negative
assertion that the outer `<div class="wp-block-button">`
wrapper does NOT carry the color class. The test title
("color on the inner link, NOT the outer wrapper") now pins
both sides of the contract.
- New buttons-justification whitelist regression test pins the
fallback behavior + the legitimate value path-through.
Tests: 563/563 PHP green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/visual-editor-renderer-blade/src/Support/BlockSupports.php`:
- Around line 403-433: The current fallback unconditionally adds a global
'border-style: solid' when any side or root reports width/style, which causes
side-only widths to render full boxes; modify the logic in BlockSupports.php to
emit side-specific fallbacks instead: while iterating the sides in the loop that
sets $hasSideWidthOrStyle, track which specific sides have a width but no
explicit style (e.g. push side names into a $sidesNeedingStyle array when you
see a width and no style for that side), preserve existing behavior for root
declarations using $hasRootWidthOrStyle, and then change the final fallback
block so that if $hasRootWidthOrStyle is true you keep the existing global
fallback using self::styleListContains('border-style:'), but if only
side-specific sides require a fallback, append 'border-{side}-style: solid' for
each side in $sidesNeedingStyle (rather than adding a global 'border-style:
solid').
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 921d18cf-a899-4110-8142-780e021c8baf
📒 Files selected for processing (4)
packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.phppackages/visual-editor-renderer-blade/src/Support/BlockSupports.phppackages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.phppackages/visual-editor-renderer-blade/tests/Unit/BlockSupportsTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/visual-editor-renderer-blade/tests/Unit/BlockSupportsTest.php
The border fallback in `BlockSupports::applyBorder` unconditionally
appended a root `border-style: solid` whenever ANY side or root
reported a width or style. That turned on the untouched edges with
browser-default widths: a top-only border declared as
`style.border.top.width: 2px` rendered as a full box instead of a
single top line.
Fix is per-side: while iterating sides, track which specific sides
have a width without an explicit style; afterward emit
`border-{side}-style: solid` only for those sides. The root-level
fallback still fires when the ROOT declaration set width or style,
since whole-box semantics are correct in that case.
Tests:
- Reworked the existing per-side test to assert the per-side fallback
AND the absence of the global one.
- Added a multi-side regression so two sides each get their own
fallback and the untouched two stay untouched.
- Added an explicit-per-side-style case proving the fallback skips
when the user supplied a style.
All 565 PHP tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Keystone #50.
The problem
Edit a block's background color (or alignment, padding, font size, border radius, etc.) in the editor → save → reload the public front-end → change is invisible. The block-attribute compilation that WP core's
apply_block_supports()does server-side wasn't ported intorenderer-blade. Block partials only emittedtagName/layout.type/className; every other persisted attribute was dropped on the floor.What this MR does
BlockSupportshelper (packages/visual-editor-renderer-blade/src/Support/BlockSupports.php) compiles a block's attributes into the canonical wrapper-attribute triple (classes[],style,id) matching WP core's serializers.align,textAlign, color (palette + custom + gradient), spacing (padding/margin/blockGap), border (radius/color/style/width root + per-side), typography (palette + custom for fontSize/fontFamily/fontWeight/fontStyle/lineHeight/letterSpacing/textTransform/textDecoration),className,anchor. Also expandsvar:preset|...shorthand into realvar(--wp--preset--…)references.wrapperAttrs()helper returns the ready-to-splice attribute string each Blade partial needs:<{{ $tag }}{!! BlockSupports::wrapperAttrs($attributes, ['wp-block-group']) !!}>.BlockSupportsinstead of hand-rollingclassName/alignsubsets. Block-specific quirks (e.g.core/spacer's height,core/cover's minHeight,core/button's wrapper-vs-link supports split) stay in the partial and merge with the compiled output.core/buttonnoteThe visual block-supports (color, border, spacing, typography) now land on the inner
<a>/<span>sotheme.json'sstyles.elements.buttontargeting.wp-element-buttonactually picks up the slug-based class set. The outer wrapper carries onlyalign/className/anchor. Mirrors WP core.Tests
BlockSupports::compileandwrapperAttrscovering each block-supports category, palette-vs-custom priority, preset-reference expansion, slug normalization, anchor sanitization, and the legacystyle: "default"defensive case forcore/separator.BlockRendererproving the rendered HTML output now carries the canonical class / style / id markers.Test plan
vendor/bin/pest --compact— 555/555 PHP green locally.npx vitest run— 1037/1037 JS green locally.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Tests