From bb73cc6bd5dd175b37f007db3423ae187a3c17a0 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 07:31:13 -0700 Subject: [PATCH 1/4] test: reproduce dropped quote border --- .../unit/block-style-support-conversion.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index 57d91d02..31054cb1 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -374,6 +374,30 @@ $compoundCandidateProbe = ( new StaticStyleParityProbe() )->extract(StaticStyleParityRunner::candidateHtmlFromSerializedBlocks($compoundPaintMarkup), $compoundPaintCssAsset); $assert(0 < (int) ($compoundSourceProbe['summary']['styled_total'] ?? 0) && 0 < (int) ($compoundCandidateProbe['summary']['styled_total'] ?? 0), '62: layered background cascade case produces nonzero source and candidate style probes', json_encode(array($compoundSourceProbe['summary'] ?? array(), $compoundCandidateProbe['summary'] ?? array()))); +$amberQuoteHtml = '
Comfort is a result, never a method
'; +$amberQuoteResult = ( new HtmlTransformer() )->transform($amberQuoteHtml, array())->toArray(); +$amberQuote = $amberQuoteResult['blocks'][0] ?? array(); +$amberQuoteAttrs = is_array($amberQuote['attrs'] ?? null) ? $amberQuote['attrs'] : array(); +$amberQuoteCss = implode("\n", array_map(static fn (array $asset): string => 'css' === ($asset['kind'] ?? '') ? (string) ($asset['content'] ?? '') : '', $amberQuoteResult['assets'] ?? array())); +$amberQuoteLeftBorder = is_array($amberQuoteAttrs['style']['border']['left'] ?? null) ? $amberQuoteAttrs['style']['border']['left'] : array(); + +$assert( + 'core/quote' === ($amberQuote['blockName'] ?? '') + && '1.2rem' === ($amberQuoteAttrs['style']['spacing']['padding']['left'] ?? '') + && 'var(--head)' === ($amberQuoteAttrs['style']['typography']['fontFamily'] ?? '') + && '2.2rem' === ($amberQuoteAttrs['style']['typography']['fontSize'] ?? '') + && '700' === ($amberQuoteAttrs['style']['typography']['fontWeight'] ?? '') + && '-.02em' === ($amberQuoteAttrs['style']['typography']['letterSpacing'] ?? ''), + 'N1 setup: amber quote exercises native spacing and typography conversion', + json_encode($amberQuote) +); +$assert( + array( 'width' => '2px', 'style' => 'solid', 'color' => 'var(--secondary)' ) === $amberQuoteLeftBorder + || str_contains($amberQuoteCss, 'border-left:2px solid var(--secondary)'), + 'N1: amber quote retains its authored left border through native support or a carrier', + json_encode(array( 'attrs' => $amberQuoteAttrs, 'css' => $amberQuoteCss )) +); + if ( $failures > 0 ) { fwrite(STDERR, "Block style support conversion tests: {$failures} failed, {$passes} passed\n"); exit(1); From 96c864ec5ec37ec3abbdf4044f2c64000e98c172 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 09:29:02 -0700 Subject: [PATCH 2/4] fix: preserve authored borders by declared support 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. --- .../wordpress-6.6-core-block-supports.json | 2772 +++++++++++++++++ .../src/HtmlToBlocks/HtmlTransformer.php | 69 + .../Style/StyleAttributeMapper.php | 156 +- .../Style/StyleResolutionTrait.php | 16 + php-transformer/src/WordPress/Runtime.php | 99 +- .../tests/contract/runtime-no-wordpress.php | 6 + .../contract/runtime-wordpress-stubs.php | 16 +- .../tests/packaging/install-proof.php | 17 +- .../unit/block-style-support-conversion.php | 140 +- .../tools/generate-core-block-supports.php | 60 + 10 files changed, 3313 insertions(+), 38 deletions(-) create mode 100644 php-transformer/resources/wordpress-6.6-core-block-supports.json create mode 100644 php-transformer/tools/generate-core-block-supports.php diff --git a/php-transformer/resources/wordpress-6.6-core-block-supports.json b/php-transformer/resources/wordpress-6.6-core-block-supports.json new file mode 100644 index 00000000..36d1175b --- /dev/null +++ b/php-transformer/resources/wordpress-6.6-core-block-supports.json @@ -0,0 +1,2772 @@ +{ + "schema": "blocks-engine/php-transformer/core-block-supports/v1", + "source": "WordPress/wordpress-develop@6.6", + "block_count": 93, + "blocks": { + "core/archives": { + "align": true, + "html": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/audio": { + "anchor": true, + "align": true, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/avatar": { + "html": false, + "align": true, + "alignWide": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "__experimentalBorder": { + "__experimentalSkipSerialization": true, + "radius": true, + "width": true, + "color": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true + } + }, + "color": { + "text": false, + "background": false, + "__experimentalDuotone": "img" + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/block": { + "customClassName": false, + "html": false, + "inserter": false, + "renaming": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/button": { + "anchor": true, + "splitting": true, + "align": false, + "alignWide": false, + "color": { + "__experimentalSkipSerialization": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "reusable": false, + "shadow": { + "__experimentalSkipSerialization": true + }, + "spacing": { + "__experimentalSkipSerialization": true, + "padding": [ + "horizontal", + "vertical" + ], + "__experimentalDefaultControls": { + "padding": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "__experimentalSelector": ".wp-block-button .wp-block-button__link", + "interactivity": { + "clientNavigation": true + } + }, + "core/buttons": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "__experimentalExposeControlsToChildren": true, + "spacing": { + "blockGap": true, + "margin": [ + "top", + "bottom" + ], + "__experimentalDefaultControls": { + "blockGap": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "default": { + "type": "flex" + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/calendar": { + "align": true, + "color": { + "link": true, + "__experimentalSkipSerialization": [ + "text", + "background" + ], + "__experimentalDefaultControls": { + "background": true, + "text": true + }, + "__experimentalSelector": "table, th" + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/categories": { + "align": true, + "html": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/code": { + "align": [ + "wide" + ], + "anchor": true, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "margin": [ + "top", + "bottom" + ], + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "width": true, + "color": true + } + }, + "color": { + "text": true, + "background": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/column": { + "__experimentalOnEnter": true, + "anchor": true, + "reusable": false, + "html": false, + "color": { + "gradients": true, + "heading": true, + "button": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "shadow": true, + "spacing": { + "blockGap": true, + "padding": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "__experimentalBorder": { + "color": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "style": true, + "width": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "layout": true, + "interactivity": { + "clientNavigation": true + } + }, + "core/columns": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "link": true, + "heading": true, + "button": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "blockGap": { + "__experimentalDefault": "2em", + "sides": [ + "horizontal", + "vertical" + ] + }, + "margin": [ + "top", + "bottom" + ], + "padding": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": false, + "default": { + "type": "flex", + "flexWrap": "nowrap" + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + }, + "shadow": true + }, + "core/comment-author-name": { + "html": false, + "spacing": { + "margin": true, + "padding": true + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comment-content": { + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "padding": [ + "horizontal", + "vertical" + ], + "__experimentalDefaultControls": { + "padding": true + } + }, + "html": false + }, + "core/comment-date": { + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comment-edit-link": { + "html": false, + "color": { + "link": true, + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comment-reply-link": { + "color": { + "gradients": true, + "link": true, + "text": false, + "__experimentalDefaultControls": { + "background": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "html": false + }, + "core/comment-template": { + "align": true, + "html": false, + "reusable": false, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comments": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "heading": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "core/comments-pagination": { + "align": true, + "reusable": false, + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "default": { + "type": "flex" + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comments-pagination-next": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comments-pagination-numbers": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comments-pagination-previous": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/comments-title": { + "anchor": false, + "align": true, + "html": false, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true + }, + "color": { + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/cover": { + "anchor": true, + "align": true, + "html": false, + "shadow": true, + "spacing": { + "padding": true, + "margin": [ + "top", + "bottom" + ], + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "color": { + "__experimentalDuotone": "> .wp-block-cover__image-background, > .wp-block-cover__video-background", + "heading": true, + "text": true, + "background": false, + "__experimentalSkipSerialization": [ + "gradients" + ], + "enableContrastChecker": false + }, + "dimensions": { + "aspectRatio": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "layout": { + "allowJustification": false + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/details": { + "__experimentalOnEnter": true, + "align": [ + "wide", + "full" + ], + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "__experimentalBorder": { + "color": true, + "width": true, + "style": true + }, + "html": false, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "layout": { + "allowEditing": false + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/embed": { + "align": true, + "spacing": { + "margin": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/file": { + "anchor": true, + "align": true, + "spacing": { + "margin": true, + "padding": true + }, + "color": { + "gradients": true, + "link": true, + "text": false, + "__experimentalDefaultControls": { + "background": true, + "link": true + } + }, + "interactivity": true + }, + "core/footnotes": { + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": false, + "color": false, + "width": false, + "style": false + } + }, + "color": { + "background": true, + "link": true, + "text": true, + "__experimentalDefaultControls": { + "link": true, + "text": true + } + }, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/freeform": { + "className": false, + "customClassName": false, + "reusable": false + }, + "core/gallery": { + "anchor": true, + "align": true, + "html": false, + "units": [ + "px", + "em", + "rem", + "vh", + "vw" + ], + "spacing": { + "margin": true, + "padding": true, + "blockGap": [ + "horizontal", + "vertical" + ], + "__experimentalSkipSerialization": [ + "blockGap" + ], + "__experimentalDefaultControls": { + "blockGap": true, + "margin": false, + "padding": false + } + }, + "color": { + "text": false, + "background": true, + "gradients": true + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": false, + "default": { + "type": "flex" + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/group": { + "__experimentalOnEnter": true, + "__experimentalOnMerge": true, + "__experimentalSettings": true, + "align": [ + "wide", + "full" + ], + "anchor": true, + "ariaLabel": true, + "html": false, + "background": { + "backgroundImage": true, + "backgroundSize": true, + "__experimentalDefaultControls": { + "backgroundImage": true + } + }, + "color": { + "gradients": true, + "heading": true, + "button": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": [ + "top", + "bottom" + ], + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "dimensions": { + "minHeight": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "position": { + "sticky": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "layout": { + "allowSizingOnChildren": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/heading": { + "align": [ + "wide", + "full" + ], + "anchor": true, + "className": true, + "splitting": true, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__unstablePasteTextInline": true, + "__experimentalSlashInserter": true, + "interactivity": { + "clientNavigation": true + } + }, + "core/home-link": { + "reusable": false, + "html": false, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/html": { + "customClassName": false, + "className": false, + "html": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/image": { + "interactivity": true, + "align": [ + "left", + "center", + "right", + "wide", + "full" + ], + "anchor": true, + "color": { + "text": false, + "background": false + }, + "filter": { + "duotone": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "width": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "width": true + } + }, + "shadow": { + "__experimentalSkipSerialization": true + } + }, + "core/latest-comments": { + "align": true, + "html": false, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/latest-posts": { + "align": true, + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/legacy-widget": { + "html": false, + "customClassName": false, + "reusable": false + }, + "core/list": { + "anchor": true, + "html": false, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "__unstablePasteTextInline": true, + "__experimentalOnMerge": true, + "__experimentalSlashInserter": true, + "interactivity": { + "clientNavigation": true + } + }, + "core/list-item": { + "className": false, + "__experimentalSelector": ".wp-block-list > li", + "splitting": true, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/loginout": { + "className": true, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/media-text": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "heading": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/missing": { + "className": false, + "customClassName": false, + "inserter": false, + "html": false, + "reusable": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/more": { + "customClassName": false, + "className": false, + "html": false, + "multiple": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/navigation": { + "align": [ + "wide", + "full" + ], + "ariaLabel": true, + "html": false, + "inserter": true, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalTextTransform": true, + "__experimentalFontFamily": true, + "__experimentalLetterSpacing": true, + "__experimentalTextDecoration": true, + "__experimentalSkipSerialization": [ + "textDecoration" + ], + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "blockGap": true, + "units": [ + "px", + "em", + "rem", + "vh", + "vw" + ], + "__experimentalDefaultControls": { + "blockGap": true + } + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowVerticalAlignment": false, + "allowSizingOnChildren": true, + "default": { + "type": "flex" + } + }, + "interactivity": true, + "renaming": false + }, + "core/navigation-link": { + "reusable": false, + "html": false, + "__experimentalSlashInserter": true, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "renaming": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/navigation-submenu": { + "reusable": false, + "html": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/nextpage": { + "customClassName": false, + "className": false, + "html": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/page-list": { + "reusable": false, + "html": false, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/page-list-item": { + "reusable": false, + "html": false, + "lock": false, + "inserter": false, + "__experimentalToolbar": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/paragraph": { + "splitting": true, + "anchor": true, + "className": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalSelector": "p", + "__unstablePasteTextInline": true, + "interactivity": { + "clientNavigation": true + } + }, + "core/pattern": { + "html": false, + "inserter": false, + "renaming": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-author": { + "html": false, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDuotone": ".wp-block-post-author__avatar img", + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-author-biography": { + "spacing": { + "margin": true, + "padding": true + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-author-name": { + "html": false, + "spacing": { + "margin": true, + "padding": true + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-comments-form": { + "html": false, + "color": { + "gradients": true, + "heading": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "core/post-content": { + "align": [ + "wide", + "full" + ], + "html": false, + "layout": true, + "dimensions": { + "minHeight": true + }, + "spacing": { + "blockGap": true + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": false, + "text": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "core/post-date": { + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-excerpt": { + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-featured-image": { + "align": [ + "left", + "right", + "center", + "wide", + "full" + ], + "color": { + "text": false, + "background": false + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "width": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "width": true + } + }, + "filter": { + "duotone": true + }, + "shadow": { + "__experimentalSkipSerialization": true + }, + "html": false, + "spacing": { + "margin": true, + "padding": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-navigation-link": { + "reusable": false, + "html": false, + "color": { + "link": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-template": { + "reusable": false, + "html": false, + "align": [ + "wide", + "full" + ], + "layout": true, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "blockGap": { + "__experimentalDefault": "1.25em" + }, + "__experimentalDefaultControls": { + "blockGap": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-terms": { + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/post-title": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/preformatted": { + "anchor": true, + "color": { + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "padding": true, + "margin": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/pullquote": { + "anchor": true, + "align": [ + "left", + "right", + "wide", + "full" + ], + "color": { + "gradients": true, + "background": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "__experimentalStyle": { + "typography": { + "fontSize": "1.5em", + "lineHeight": "1.6" + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query": { + "align": [ + "wide", + "full" + ], + "html": false, + "layout": true, + "interactivity": true + }, + "core/query-no-results": { + "align": true, + "reusable": false, + "html": false, + "color": { + "gradients": true, + "link": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query-pagination": { + "align": true, + "reusable": false, + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "default": { + "type": "flex" + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query-pagination-next": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query-pagination-numbers": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query-pagination-previous": { + "reusable": false, + "html": false, + "color": { + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/query-title": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/quote": { + "anchor": true, + "html": false, + "__experimentalOnEnter": true, + "__experimentalOnMerge": true, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "color": { + "gradients": true, + "heading": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "layout": { + "allowEditing": false + }, + "spacing": { + "blockGap": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/read-more": { + "html": false, + "color": { + "gradients": true, + "text": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalLetterSpacing": true, + "__experimentalTextDecoration": true, + "__experimentalDefaultControls": { + "fontSize": true, + "textDecoration": true + } + }, + "spacing": { + "margin": [ + "top", + "bottom" + ], + "padding": true, + "__experimentalDefaultControls": { + "padding": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "width": true, + "__experimentalDefaultControls": { + "width": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/rss": { + "align": true, + "html": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/search": { + "align": [ + "left", + "center", + "right" + ], + "color": { + "gradients": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "interactivity": true, + "typography": { + "__experimentalSkipSerialization": true, + "__experimentalSelector": ".wp-block-search__label, .wp-block-search__input, .wp-block-search__button", + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "width": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "width": true + } + }, + "html": false + }, + "core/separator": { + "anchor": true, + "align": [ + "center", + "wide", + "full" + ], + "color": { + "enableContrastChecker": false, + "__experimentalSkipSerialization": true, + "gradients": true, + "background": true, + "text": false, + "__experimentalDefaultControls": { + "background": true + } + }, + "spacing": { + "margin": [ + "top", + "bottom" + ] + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/shortcode": { + "className": false, + "customClassName": false, + "html": false + }, + "core/site-logo": { + "html": false, + "align": true, + "alignWide": false, + "color": { + "__experimentalDuotone": "img, .components-placeholder__illustration, .components-placeholder::before", + "text": false, + "background": false + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/site-tagline": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/site-title": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "padding": true, + "margin": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/social-link": { + "reusable": false, + "html": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/social-links": { + "align": [ + "left", + "center", + "right" + ], + "anchor": true, + "__experimentalExposeControlsToChildren": true, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowVerticalAlignment": false, + "default": { + "type": "flex" + } + }, + "color": { + "enableContrastChecker": false, + "background": true, + "gradients": true, + "text": false, + "__experimentalDefaultControls": { + "background": false + } + }, + "spacing": { + "blockGap": [ + "horizontal", + "vertical" + ], + "margin": true, + "padding": true, + "units": [ + "px", + "em", + "rem", + "vh", + "vw" + ], + "__experimentalDefaultControls": { + "blockGap": true, + "margin": true, + "padding": false + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/spacer": { + "anchor": true, + "spacing": { + "margin": [ + "top", + "bottom" + ], + "__experimentalDefaultControls": { + "margin": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/table": { + "anchor": true, + "align": true, + "color": { + "__experimentalSkipSerialization": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "__experimentalSkipSerialization": true, + "color": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "style": true, + "width": true + } + }, + "__experimentalSelector": ".wp-block-table > table", + "interactivity": { + "clientNavigation": true + } + }, + "core/tag-cloud": { + "html": false, + "align": true, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalLetterSpacing": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/template-part": { + "align": true, + "html": false, + "reusable": false, + "renaming": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/term-description": { + "align": [ + "wide", + "full" + ], + "html": false, + "color": { + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "padding": true, + "margin": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/text-columns": { + "inserter": false, + "interactivity": { + "clientNavigation": true + } + }, + "core/verse": { + "anchor": true, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "__experimentalFontFamily": true, + "lineHeight": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "__experimentalBorder": { + "radius": true, + "width": true, + "color": true, + "style": true + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/video": { + "anchor": true, + "align": true, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "core/widget-group": { + "html": false, + "inserter": true, + "customClassName": true, + "reusable": false + } + } +} diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 0125fd83..10a0b185 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -3648,6 +3648,7 @@ private function createBlock(string $name, array $attrs = array(), array $innerB $attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $nativeButtonMarker); $this->registerNativeButtonStyleRule($nativeButtonMarker, $hasNativeButtonColor ? $attrs : array(), $nativeButtonTextAlignment); } + $attrs = $this->applyDeclaredBorderSupport($name, $attrs, $sourceElement); $provenanceId = $this->nextSourceProvenanceId++; $this->recordPresentationProvenance($name, $attrs, $sourceElement); $this->recordStructureProvenance($name, $attrs, $sourceElement); @@ -3675,6 +3676,74 @@ private function createBlock(string $name, array $attrs = array(), array $innerB return $block; } + /** + * WordPress ignores style.border components that the registered block type + * does not declare. Keep supported components native; move only unsupported + * width/style/color values into the existing deterministic carrier. Border + * radius deliberately stays on the pre-existing native path unchanged. + * + * @param array $attrs + * @return array + */ + private function applyDeclaredBorderSupport(string $name, array $attrs, DOMElement $sourceElement): array + { + $border = is_array($attrs['style']['border'] ?? null) ? $attrs['style']['border'] : array(); + if ( array() === $border ) { + return $attrs; + } + + $fallback = array(); + foreach ( array( 'width', 'style', 'color' ) as $component ) { + if ( ! array_key_exists($component, $border) || $this->runtime->blockSupportsBorder($name, $component) ) { + continue; + } + $fallback[ $component ] = $border[ $component ]; + unset($border[ $component ]); + } + foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { + $sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array(); + foreach ( array( 'width', 'style', 'color' ) as $component ) { + if ( ! array_key_exists($component, $sideBorder) || $this->runtime->blockSupportsBorder($name, $component) ) { + continue; + } + $fallback[ $side ][ $component ] = $sideBorder[ $component ]; + unset($sideBorder[ $component ]); + } + if ( array() === $sideBorder ) { + unset($border[ $side ]); + } else { + $border[ $side ] = $sideBorder; + } + } + + if ( array() === $fallback ) { + return $attrs; + } + + $fallbackStyle = $this->styleAttributeMapper()->serialize(array( 'border' => $fallback ))['style']; + $fallbackDeclarations = $this->cssDeclarations($fallbackStyle); + $carrier = $this->inlineGeometryClassName( + $sourceElement, + array(), + array_keys($fallbackDeclarations), + $fallbackDeclarations + ); + if ( '' !== $carrier ) { + $attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $carrier); + } + + if ( array() === $border ) { + unset($attrs['style']['border']); + } else { + $attrs['style']['border'] = $border; + } + if ( empty($attrs['style']) ) { + unset($attrs['style']); + } + + return $attrs; + } + /** * Project the inherited foreground because core/button supplies a default link * color. Text alignment uses the same scoped link rule for direct and inherited diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php b/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php index 492d26f1..ba4c88c0 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php @@ -168,6 +168,19 @@ public function serialize(array $style): array if ( '' !== trim((string) ($border['radius'] ?? '')) ) { $declarations[] = 'border-radius:' . trim((string) $border['radius']); } + foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { + $sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array(); + if ( '' !== trim((string) ($sideBorder['color'] ?? '')) ) { + $classes[] = 'has-border-color'; + $declarations[] = 'border-' . $side . '-color:' . trim((string) $sideBorder['color']); + } + if ( '' !== trim((string) ($sideBorder['style'] ?? '')) ) { + $declarations[] = 'border-' . $side . '-style:' . trim((string) $sideBorder['style']); + } + if ( '' !== trim((string) ($sideBorder['width'] ?? '')) ) { + $declarations[] = 'border-' . $side . '-width:' . trim((string) $sideBorder['width']); + } + } // Match the core style engine: dimensions precede spacing in save(). $dimensions = is_array($style['dimensions'] ?? null) ? $style['dimensions'] : array(); @@ -448,23 +461,22 @@ private function boxSides(string $property, array $declarations, array &$consume private function border(array $declarations, array &$consumed): array { $border = array(); - $shorthand = $this->parseBorderShorthand((string) ($declarations['border'] ?? '')); - if ( isset($declarations['border']) ) { - $consumed['border'] = true; - } - - $width = trim((string) ($declarations['border-width'] ?? $shorthand['width'] ?? '')); - if ( '' === $width ) { - $width = $this->uniformBorderSideValue('width', $declarations, $consumed); - } - $style = strtolower(trim((string) ($declarations['border-style'] ?? $shorthand['style'] ?? ''))); - $colorValue = $this->cssColor((string) ($declarations['border-color'] ?? $shorthand['color'] ?? '')); - foreach ( array( 'border-width', 'border-style', 'border-color' ) as $name ) { - if ( isset($declarations[ $name ]) ) { + $positions = array_flip(array_keys($declarations)); + foreach ( array_keys($declarations) as $name ) { + if ( $this->isMappedBorderProperty($name) ) { $consumed[ $name ] = true; } } + $global = array(); + foreach ( array( 'width', 'style', 'color' ) as $component ) { + $global[ $component ] = $this->borderComponentCandidate($declarations, $positions, $component); + } + + $width = trim($global['width']['value']); + $style = strtolower(trim($global['style']['value'])); + $colorValue = $this->cssColor($global['color']['value']); + $noBorder = 'none' === $style || ( '' !== $width && (float) $width === 0.0 && '' === $colorValue && '' === $style ); if ( ! $noBorder ) { if ( '' !== $width && (float) $width !== 0.0 ) { @@ -477,35 +489,127 @@ private function border(array $declarations, array &$consumed): array $border['color'] = $colorValue; } } + $hasGlobalBorder = isset($border['width']) || isset($border['style']) || isset($border['color']); $radius = trim((string) ($declarations['border-radius'] ?? '')); if ( '' !== $radius ) { - $consumed['border-radius'] = true; - $border['radius'] = $radius; + $border['radius'] = $radius; } + foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { + $sideComponents = array(); + foreach ( array( 'width', 'style', 'color' ) as $component ) { + $candidate = $this->borderComponentCandidate($declarations, $positions, $component, $side); + if ( $candidate['index'] > $global[ $component ]['index'] ) { + $sideComponents[ $component ] = $candidate['value']; + } + } + + $sideWidth = trim((string) ($sideComponents['width'] ?? '')); + $sideStyle = strtolower(trim((string) ($sideComponents['style'] ?? ''))); + $sideColor = $this->cssColor((string) ($sideComponents['color'] ?? '')); + + $sideValues = array(); + $noSideBorder = 'none' === $sideStyle || ( '' !== $sideWidth && (float) $sideWidth === 0.0 && '' === $sideColor && '' === $sideStyle ); + if ( ! $noSideBorder || $hasGlobalBorder ) { + if ( '' !== $sideWidth && ( (float) $sideWidth !== 0.0 || $hasGlobalBorder ) ) { + $sideValues['width'] = $sideWidth; + } + if ( '' !== $sideStyle && ( 'none' !== $sideStyle || $hasGlobalBorder ) ) { + $sideValues['style'] = $sideStyle; + } + if ( '' !== $sideColor ) { + $sideValues['color'] = $sideColor; + } + } + if ( array() !== $sideValues ) { + $border[ $side ] = $sideValues; + } + } + + $this->collapseUniformBorderSideComponent($border, 'width'); + return $border; } /** - * Collapse equal physical side values into the canonical border support. - * Unequal sides remain under author stylesheet ownership. + * Return the last authored global or per-side value for one border component. + * A shorthand participates even when it omits the component because CSS + * shorthands reset omitted values to their initial state. * * @param array $declarations - * @param array $consumed + * @param array $positions + * @return array{value: string, index: int} */ - private function uniformBorderSideValue(string $property, array $declarations, array &$consumed): string + private function borderComponentCandidate(array $declarations, array $positions, string $component, string $side = ''): array { - $names = array_map(static fn (string $side): string => 'border-' . $side . '-' . $property, array( 'top', 'right', 'bottom', 'left' )); - $values = array_map(static fn (string $name): string => trim((string) ($declarations[ $name ] ?? '')), $names); - if ( in_array('', $values, true) || 1 !== count(array_unique($values)) ) { - return ''; + $shorthandName = '' === $side ? 'border' : 'border-' . $side; + $longhandName = $shorthandName . '-' . $component; + $candidate = array( 'value' => '', 'index' => -1 ); + + if ( isset($declarations[ $shorthandName ]) ) { + $shorthand = $this->parseBorderShorthand($declarations[ $shorthandName ]); + $initialValues = array( + 'width' => 'medium', + 'style' => 'none', + 'color' => 'currentColor', + ); + $candidate = array( + 'value' => array() === $shorthand ? '' : (string) ($shorthand[ $component ] ?? $initialValues[ $component ]), + 'index' => $positions[ $shorthandName ], + ); + } + + if ( isset($declarations[ $longhandName ]) && $positions[ $longhandName ] > $candidate['index'] ) { + $candidate = array( + 'value' => $declarations[ $longhandName ], + 'index' => $positions[ $longhandName ], + ); + } + + return $candidate; + } + + private function isMappedBorderProperty(string $name): bool + { + if ( in_array($name, array( 'border', 'border-width', 'border-style', 'border-color', 'border-radius' ), true) ) { + return true; + } + + return (bool) preg_match('/^border-(?:top|right|bottom|left)(?:-(?:width|style|color))?$/', $name); + } + + /** + * Collapse four equal physical side values into one canonical component and + * remove the now-redundant side objects. + * + * @param array $border + */ + private function collapseUniformBorderSideComponent(array &$border, string $component): void + { + $sides = array( 'top', 'right', 'bottom', 'left' ); + $values = array(); + foreach ( $sides as $side ) { + $sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array(); + $value = trim((string) ($sideBorder[ $component ] ?? '')); + if ( '' === $value ) { + return; + } + $values[] = $value; } - foreach ( $names as $name ) { - $consumed[ $name ] = true; + if ( 1 !== count(array_unique($values)) ) { + return; + } + + foreach ( $sides as $side ) { + unset($border[ $side ][ $component ]); + if ( array() === $border[ $side ] ) { + unset($border[ $side ]); + } } - return $values[0]; + unset($border[ $component ]); + $border = array( $component => $values[0] ) + $border; } /** diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php index 8749a241..a9cd2e29 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php @@ -1695,14 +1695,26 @@ private function safeVisualDeclarations(array $declarations): array 'background-size', 'aspect-ratio', 'border', + 'border-bottom', + 'border-bottom-color', + 'border-bottom-style', 'border-color', + 'border-left', + 'border-left-color', + 'border-left-style', 'border-radius', + 'border-right', + 'border-right-color', + 'border-right-style', 'border-style', 'border-bottom-width', 'border-collapse', 'border-left-width', 'border-right-width', 'border-spacing', + 'border-top', + 'border-top-color', + 'border-top-style', 'border-top-width', 'border-width', 'box-shadow', @@ -1775,6 +1787,10 @@ private function cssDeclarations(string $style): array $value = preg_replace('/\s+/', ' ', $value) ?? $value; $allowsImageUrl = in_array($name, array( 'background', 'background-image', 'list-style', 'list-style-image' ), true) && ! preg_match('/(?:expression\s*\(|javascript\s*:)/i', $value); if ( '' !== $name && '' !== $value && ( $allowsImageUrl || ! preg_match('/(?:expression\s*\(|javascript\s*:|url\s*\()/i', $value) ) ) { + // Keep the surviving declaration at its final authored position. + // Border shorthands and longhands reset one another in source + // order, so overwriting a prior key in place is not sufficient. + unset($declarations[$name]); $declarations[$name] = $value; } } diff --git a/php-transformer/src/WordPress/Runtime.php b/php-transformer/src/WordPress/Runtime.php index 36cdd20f..e955d196 100644 --- a/php-transformer/src/WordPress/Runtime.php +++ b/php-transformer/src/WordPress/Runtime.php @@ -55,6 +55,9 @@ final class Runtime */ private array $diagnostics = array(); + /** @var array>|null */ + private ?array $fallbackCoreBlockSupports = null; + public function hasWordPress(): bool { return $this->canParseBlocks() @@ -123,21 +126,41 @@ public function availableCoreBlockNames(): array } /** - * @return array + * Whether the registered block type declares support for one authored border + * component. WordPress still exposes border support under the historical + * `__experimentalBorder` key in block.json; accept the stabilized `border` + * key as well. Unknown block metadata fails closed so callers can retain the + * declaration through a CSS carrier instead of emitting an ignored attribute. */ - private function registeredCoreBlockNames(): array + public function blockSupportsBorder(string $blockName, string $component): bool { - if ( ! class_exists('WP_Block_Type_Registry') || ! method_exists('WP_Block_Type_Registry', 'get_instance') ) { - return array(); + if ( ! in_array($component, array( 'color', 'style', 'width' ), true) ) { + return false; } - $registry = \WP_Block_Type_Registry::get_instance(); - if ( ! is_object($registry) || ! method_exists($registry, 'get_all_registered') ) { - return array(); + $supports = $this->registeredBlockSupports($blockName); + if ( null === $supports ) { + $supports = $this->fallbackBlockSupports($blockName); + } + if ( null === $supports ) { + return false; } + $border = $supports['border'] ?? $supports['__experimentalBorder'] ?? false; + if ( true === $border ) { + return true; + } + + return is_array($border) && true === ($border[ $component ] ?? false); + } + + /** + * @return array + */ + private function registeredCoreBlockNames(): array + { $names = array(); - foreach ( $registry->get_all_registered() as $key => $blockType ) { + foreach ( $this->registeredBlockTypes() as $key => $blockType ) { $name = is_string($key) ? $key : ''; if ( '' === $name && is_object($blockType) && isset($blockType->name) && is_string($blockType->name) ) { $name = $blockType->name; @@ -154,6 +177,66 @@ private function registeredCoreBlockNames(): array return $names; } + /** + * @return array + */ + private function registeredBlockTypes(): array + { + if ( ! class_exists('WP_Block_Type_Registry') || ! method_exists('WP_Block_Type_Registry', 'get_instance') ) { + return array(); + } + + $registry = \WP_Block_Type_Registry::get_instance(); + if ( ! is_object($registry) || ! method_exists($registry, 'get_all_registered') ) { + return array(); + } + + $registered = $registry->get_all_registered(); + return is_array($registered) ? $registered : array(); + } + + /** + * Resolve support from the block type's registered declaration, never from a + * transformer-owned block-name allowlist. + * + * @return array|null + */ + private function registeredBlockSupports(string $blockName): ?array + { + foreach ( $this->registeredBlockTypes() as $key => $blockType ) { + $name = is_string($key) ? $key : ''; + if ( '' === $name && is_object($blockType) && isset($blockType->name) && is_string($blockType->name) ) { + $name = $blockType->name; + } + if ( $blockName !== $name || ! is_object($blockType) ) { + continue; + } + + return is_array($blockType->supports ?? null) ? $blockType->supports : array(); + } + + return null; + } + + /** + * Standalone transforms have no WP_Block_Type_Registry. Load the generated + * WordPress 6.6 declaration snapshot so the same block.json support check is + * still available. Live registered declarations always take precedence. + * + * @return array|null + */ + private function fallbackBlockSupports(string $blockName): ?array + { + if ( null === $this->fallbackCoreBlockSupports ) { + $path = dirname(__DIR__, 2) . '/resources/wordpress-6.6-core-block-supports.json'; + $registry = is_file($path) ? json_decode((string) file_get_contents($path), true) : null; + $this->fallbackCoreBlockSupports = is_array($registry['blocks'] ?? null) ? $registry['blocks'] : array(); + } + + $supports = $this->fallbackCoreBlockSupports[ $blockName ] ?? null; + return is_array($supports) ? $supports : null; + } + /** * @return array> */ diff --git a/php-transformer/tests/contract/runtime-no-wordpress.php b/php-transformer/tests/contract/runtime-no-wordpress.php index d1870c62..e5de6c76 100644 --- a/php-transformer/tests/contract/runtime-no-wordpress.php +++ b/php-transformer/tests/contract/runtime-no-wordpress.php @@ -12,6 +12,12 @@ assertSame(true, in_array('core/accordion', $availableCoreBlocks, true), 'Fallback native target metadata should include core/accordion.'); assertSame(true, in_array('core/icon', $availableCoreBlocks, true), 'Fallback native target metadata should include core/icon.'); assertSame(true, in_array('core/math', $availableCoreBlocks, true), 'Fallback native target metadata should include core/math.'); +assertSame(true, $runtime->blockSupportsBorder('core/group', 'width'), 'Standalone support resolution should load Group width support from the generated WordPress declaration snapshot.'); +assertSame(true, $runtime->blockSupportsBorder('core/group', 'style'), 'Standalone support resolution should load Group style support from the generated WordPress declaration snapshot.'); +assertSame(true, $runtime->blockSupportsBorder('core/group', 'color'), 'Standalone support resolution should load Group color support from the generated WordPress declaration snapshot.'); +assertSame(false, $runtime->blockSupportsBorder('core/quote', 'width'), 'Standalone support resolution should treat WordPress 6.6 Quote as lacking border support.'); +assertSame(true, $runtime->blockSupportsBorder('core/image', 'width'), 'Standalone support resolution should load Image width support from its declaration.'); +assertSame(false, $runtime->blockSupportsBorder('core/image', 'style'), 'Standalone support resolution should honor Image declarations that omit border style support.'); $blocks = $runtime->parseBlocks('

Hello

'); assertSame('core/paragraph', $blocks[0]['blockName'] ?? null, 'Fallback parser should parse serialized block comments.'); diff --git a/php-transformer/tests/contract/runtime-wordpress-stubs.php b/php-transformer/tests/contract/runtime-wordpress-stubs.php index 837fa953..0861f2cd 100644 --- a/php-transformer/tests/contract/runtime-wordpress-stubs.php +++ b/php-transformer/tests/contract/runtime-wordpress-stubs.php @@ -99,6 +99,17 @@ public function get_all_registered(): array 'plugin/card' => (object) array('name' => 'plugin/card'), (object) array('name' => 'core/math'), 'core/accordion' => (object) array('name' => 'core/accordion'), + 'core/group' => (object) array( + 'name' => 'core/group', + 'supports' => array( + '__experimentalBorder' => array( + 'color' => true, + 'style' => false, + 'width' => true, + ), + ), + ), + 'core/quote' => (object) array('name' => 'core/quote', 'supports' => array()), ); } } @@ -120,7 +131,10 @@ public function get_all_registered(): array assertSame('{"stub":{"path":"/demo"}}', $runtime->encodeJson(array('path' => '/demo')), 'Runtime should delegate JSON encoding to wp_json_encode().'); assertSame('stub html ', $runtime->escapeHtml(''), 'Runtime should delegate HTML escaping to esc_html().'); assertSame('stub attr "value"', $runtime->escapeAttribute('"value"'), 'Runtime should delegate attribute escaping to esc_attr().'); -assertSame(array('core/accordion', 'core/icon', 'core/math'), $runtime->availableCoreBlockNames(), 'Runtime should expose registered core block names as native targets.'); +assertSame(array('core/accordion', 'core/group', 'core/icon', 'core/math', 'core/quote'), $runtime->availableCoreBlockNames(), 'Runtime should expose registered core block names as native targets.'); +assertSame(true, $runtime->blockSupportsBorder('core/group', 'width'), 'Runtime should resolve border width from the registered Group declaration.'); +assertSame(false, $runtime->blockSupportsBorder('core/group', 'style'), 'Registered Group metadata should override the standalone snapshot component by component.'); +assertSame(false, $runtime->blockSupportsBorder('core/quote', 'width'), 'A registered Quote declaration without border support should fail closed.'); fwrite(STDOUT, "WordPress runtime stub contract passed.\n"); diff --git a/php-transformer/tests/packaging/install-proof.php b/php-transformer/tests/packaging/install-proof.php index e2b49a64..3e3b5fe5 100644 --- a/php-transformer/tests/packaging/install-proof.php +++ b/php-transformer/tests/packaging/install-proof.php @@ -40,6 +40,7 @@ 'docs/contracts/php-transformer-visual-parity-fixture.schema.json', 'docs/contracts/php-transformer-visual-parity-report.schema.json', 'docs/contracts/visual-parity-report.md', + 'resources/wordpress-6.6-core-block-supports.json', 'tools/visual-parity/package-lock.json', 'tools/visual-parity/package.json', 'tools/visual-parity/bin/visual-parity.mjs', @@ -53,11 +54,25 @@ exit(1); } } -$result = (new Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer())->transform('

Proof

')->toArray(); +$transformer = new Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer(); +$result = $transformer->transform('

Proof

')->toArray(); if ('success' !== ($result['status'] ?? '') || '' === ($result['serialized_blocks'] ?? '')) { fwrite(STDERR, "php-transformer install proof failed\n"); exit(1); } +$group = $transformer->transform('

Native

')->toArray(); +$groupBorder = $group['blocks'][0]['attrs']['style']['border']['left'] ?? null; +if (array('width' => '2px', 'style' => 'solid', 'color' => 'red') !== $groupBorder) { + fwrite(STDERR, "php-transformer install proof failed native border support\n"); + exit(1); +} +$quote = $transformer->transform('
Fallback
')->toArray(); +$quoteAttrs = $quote['blocks'][0]['attrs'] ?? array(); +$quoteCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), $quote['assets'] ?? array())); +if (isset($quoteAttrs['style']['border']) || ! str_contains((string) ($quoteAttrs['className'] ?? ''), 'be-inline-geometry-') || ! str_contains($quoteCss, 'border-left-width:2px !important')) { + fwrite(STDERR, "php-transformer install proof failed unsupported border fallback\n"); + exit(1); +} PHP; run($proofRoot, array(PHP_BINARY, '-r', $smoke)); diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index 31054cb1..996a1047 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -54,7 +54,11 @@ 'border-style' => 'solid', 'border-color' => '#ffffff', )); -$assert('12.808px' === ($uniformBorder['style']['border']['width'] ?? ''), '8a: equal physical border widths collapse into canonical border support', json_encode($uniformBorder)); +$assert( + array( 'width' => '12.808px', 'style' => 'solid', 'color' => '#ffffff' ) === ($uniformBorder['style']['border'] ?? array()), + '8a: equal physical border widths collapse without redundant side objects', + json_encode($uniformBorder) +); $classBorderImage = ( new HtmlTransformer() )->transform( 'Portrait', @@ -393,11 +397,143 @@ ); $assert( array( 'width' => '2px', 'style' => 'solid', 'color' => 'var(--secondary)' ) === $amberQuoteLeftBorder - || str_contains($amberQuoteCss, 'border-left:2px solid var(--secondary)'), + || ( + str_contains($amberQuoteCss, 'border-left-color:var(--secondary) !important') + && str_contains($amberQuoteCss, 'border-left-style:solid !important') + && str_contains($amberQuoteCss, 'border-left-width:2px !important') + ), 'N1: amber quote retains its authored left border through native support or a carrier', json_encode(array( 'attrs' => $amberQuoteAttrs, 'css' => $amberQuoteCss )) ); +$borderMapper = new StyleAttributeMapper(); +$shorthandBorder = $borderMapper->map(array( 'border' => '2px solid red' )); +$leftBorder = $borderMapper->map(array( 'border-left' => '2px solid var(--secondary)' )); +$individualBorder = $borderMapper->map(array( + 'border-width' => '3px', + 'border-style' => 'dashed', + 'border-color' => '#123456', +)); +$globalThenLeftBorder = $borderMapper->map(array( + 'border' => '4px dashed blue', + 'border-left' => '2px solid red', +)); +$leftThenGlobalBorder = $borderMapper->map(array( + 'border-left' => '2px solid red', + 'border' => '4px dashed blue', +)); +$globalThenNoLeftBorder = $borderMapper->map(array( + 'border' => '4px solid blue', + 'border-left' => 'none', +)); +$globalThenColorOnlyLeftBorder = $borderMapper->map(array( + 'border' => '4px solid blue', + 'border-left' => 'red', +)); +$assert( + array( 'width' => '2px', 'style' => 'solid', 'color' => 'red' ) === ($shorthandBorder['style']['border'] ?? array()), + 'N2: border shorthand maps to native width, style, and color', + json_encode($shorthandBorder) +); +$assert( + array( 'left' => array( 'width' => '2px', 'style' => 'solid', 'color' => 'var(--secondary)' ) ) === ($leftBorder['style']['border'] ?? array()), + 'N2: per-side border shorthand maps to the native left-side object', + json_encode($leftBorder) +); +$assert( + array( 'width' => '3px', 'style' => 'dashed', 'color' => '#123456' ) === ($individualBorder['style']['border'] ?? array()), + 'N2: individual border width, style, and color forms map natively', + json_encode($individualBorder) +); +$assert( + array( + 'width' => '4px', + 'style' => 'dashed', + 'color' => 'blue', + 'left' => array( 'width' => '2px', 'style' => 'solid', 'color' => 'red' ), + ) === ($globalThenLeftBorder['style']['border'] ?? array()), + 'N2: a later per-side shorthand overrides an earlier global shorthand', + json_encode($globalThenLeftBorder) +); +$assert( + array( 'width' => '4px', 'style' => 'dashed', 'color' => 'blue' ) === ($leftThenGlobalBorder['style']['border'] ?? array()), + 'N2: a later global shorthand resets an earlier per-side shorthand', + json_encode($leftThenGlobalBorder) +); +$assert( + 'none' === ($globalThenNoLeftBorder['style']['border']['left']['style'] ?? ''), + 'N2: a later none side shorthand actively cancels an earlier global border', + json_encode($globalThenNoLeftBorder) +); +$assert( + array( 'width' => 'medium', 'style' => 'none', 'color' => 'red' ) === ($globalThenColorOnlyLeftBorder['style']['border']['left'] ?? array()), + 'N2: a partial side shorthand resets omitted components instead of inheriting the global border', + json_encode($globalThenColorOnlyLeftBorder) +); + +$nativeBorderGroupResult = ( new HtmlTransformer() )->transform( + '

Native border

', + array() +)->toArray(); +$nativeBorderGroup = $nativeBorderGroupResult['blocks'][0] ?? array(); +$nativeBorderGroupAttrs = is_array($nativeBorderGroup['attrs'] ?? null) ? $nativeBorderGroup['attrs'] : array(); +$nativeBorderGroupMarkup = (string) ($nativeBorderGroupResult['serialized_blocks'] ?? ''); +$assert( + 'core/group' === ($nativeBorderGroup['blockName'] ?? '') + && array( 'width' => '2px', 'style' => 'solid', 'color' => 'var(--secondary)' ) === ($nativeBorderGroupAttrs['style']['border']['left'] ?? array()) + && ! str_contains((string) ($nativeBorderGroupAttrs['className'] ?? ''), 'be-inline-geometry-') + && str_contains($nativeBorderGroupMarkup, 'border-left-color:var(--secondary)') + && str_contains($nativeBorderGroupMarkup, 'border-left-style:solid') + && str_contains($nativeBorderGroupMarkup, 'border-left-width:2px'), + 'N2: a border-supporting Group serializes the native per-side border', + json_encode(array( 'block' => $nativeBorderGroup, 'markup' => $nativeBorderGroupMarkup )) +); + +$matchedNativeBorderGroupResult = ( new HtmlTransformer() )->transform( + '

Matched native border

', + array( 'static_css' => '.matched-border{border-left:3px dotted #123456}' ) +)->toArray(); +$matchedNativeBorderGroup = $matchedNativeBorderGroupResult['blocks'][0] ?? array(); +$matchedNativeBorderGroupAttrs = is_array($matchedNativeBorderGroup['attrs'] ?? null) ? $matchedNativeBorderGroup['attrs'] : array(); +$assert( + array( 'width' => '3px', 'style' => 'dotted', 'color' => '#123456' ) === ($matchedNativeBorderGroupAttrs['style']['border']['left'] ?? array()) + && ! str_contains((string) ($matchedNativeBorderGroupAttrs['className'] ?? ''), 'be-inline-geometry-'), + 'N2: matched stylesheet per-side shorthand reaches native Group border support', + json_encode($matchedNativeBorderGroupResult) +); + +$repeatedBorderGroupResult = ( new HtmlTransformer() )->transform( + '

Ordered native border

', + array() +)->toArray(); +$repeatedBorderGroupAttrs = $repeatedBorderGroupResult['blocks'][0]['attrs'] ?? array(); +$assert( + '4px' === ($repeatedBorderGroupAttrs['style']['border']['width'] ?? '') + && 'dashed' === ($repeatedBorderGroupAttrs['style']['border']['style'] ?? '') + && 'blue' === ($repeatedBorderGroupAttrs['style']['border']['color'] ?? '') + && array( 'width' => '1px', 'style' => 'dotted', 'color' => 'green' ) === ($repeatedBorderGroupAttrs['style']['border']['left'] ?? array()), + 'N2: a repeated side shorthand after a global shorthand keeps its final authored position', + json_encode($repeatedBorderGroupResult) +); + +$radiusRoundTrip = $borderMapper->map(array( 'border-radius' => 'var(--radius)' )); +$assert( + array( 'radius' => 'var(--radius)' ) === ($radiusRoundTrip['style']['border'] ?? array()) + && 'border-radius:var(--radius)' === $borderMapper->serialize($radiusRoundTrip['style'])['style'], + 'N3: existing radius mapping and serialization round-trip unchanged', + json_encode($radiusRoundTrip) +); + +$assert( + ! isset($amberQuoteAttrs['style']['border']) + && str_contains((string) ($amberQuoteAttrs['className'] ?? ''), 'be-inline-geometry-') + && str_contains($amberQuoteCss, 'border-left-color:var(--secondary) !important') + && str_contains($amberQuoteCss, 'border-left-style:solid !important') + && str_contains($amberQuoteCss, 'border-left-width:2px !important'), + 'N4: a Quote without declared border support uses the carrier and emits no ignored native border attribute', + json_encode(array( 'attrs' => $amberQuoteAttrs, 'css' => $amberQuoteCss )) +); + if ( $failures > 0 ) { fwrite(STDERR, "Block style support conversion tests: {$failures} failed, {$passes} passed\n"); exit(1); diff --git a/php-transformer/tools/generate-core-block-supports.php b/php-transformer/tools/generate-core-block-supports.php new file mode 100644 index 00000000..cc79ce5f --- /dev/null +++ b/php-transformer/tools/generate-core-block-supports.php @@ -0,0 +1,60 @@ + + */ + +if ( 4 !== count($argv) ) { + fwrite(STDERR, "Usage: php tools/generate-core-block-supports.php \n"); + exit(2); +} + +$blocksDirectory = rtrim($argv[1], DIRECTORY_SEPARATOR); +$outputPath = $argv[2]; +$sourceRef = trim($argv[3]); +if ( ! is_dir($blocksDirectory) || '' === $sourceRef ) { + fwrite(STDERR, "A readable WordPress blocks directory and non-empty source ref are required.\n"); + exit(2); +} + +$blocks = array(); +foreach ( (array) glob($blocksDirectory . '/*/block.json') as $blockJsonPath ) { + $metadata = json_decode((string) file_get_contents($blockJsonPath), true); + if ( ! is_array($metadata) || ! is_string($metadata['name'] ?? null) || ! str_starts_with($metadata['name'], 'core/') ) { + continue; + } + + $blocks[ $metadata['name'] ] = is_array($metadata['supports'] ?? null) ? $metadata['supports'] : array(); +} +ksort($blocks, SORT_STRING); + +$registry = array( + 'schema' => 'blocks-engine/php-transformer/core-block-supports/v1', + 'source' => $sourceRef, + 'block_count' => count($blocks), + 'blocks' => $blocks, +); +$json = json_encode($registry, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); +if ( false === $json ) { + fwrite(STDERR, "Unable to encode the core-block support registry.\n"); + exit(1); +} + +$outputDirectory = dirname($outputPath); +if ( ! is_dir($outputDirectory) && ! mkdir($outputDirectory, 0777, true) && ! is_dir($outputDirectory) ) { + fwrite(STDERR, "Unable to create output directory: {$outputDirectory}\n"); + exit(1); +} +if ( false === file_put_contents($outputPath, $json . "\n") ) { + fwrite(STDERR, "Unable to write core-block support registry: {$outputPath}\n"); + exit(1); +} + +fwrite(STDOUT, sprintf("Core block support registry: %d declarations written to %s\n", count($blocks), $outputPath)); From 9f505b6df1edf667b57e8d1b325184ce9f91bc84 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 18:22:52 -0700 Subject: [PATCH 3/4] test: reproduce the phantom medium border from has-border-color ## 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. --- .../unit/block-style-support-conversion.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index 996a1047..889aba79 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -534,6 +534,42 @@ json_encode(array( 'attrs' => $amberQuoteAttrs, 'css' => $amberQuoteCss )) ); +// The core style engine attaches `has-border-color` to the uniform +// `border.color` definition only; `border.{side}` carries no classnames. The +// class is an all-sides signal because core ships +// `html :where(.has-border-color){border-style:solid}` in +// wp-includes/css/dist/block-library/common.css. Emitting it for a one-sided +// authored border makes WordPress paint the three unauthored sides at the +// initial `medium` width (3px) in `currentColor`, growing the box by 6px. +$sideOnlyBorderSerialized = $borderMapper->serialize(array( + 'border' => array( 'left' => array( 'width' => '2px', 'style' => 'solid', 'color' => 'var(--secondary)' ) ), +)); +$assert( + '' === $sideOnlyBorderSerialized['classes'], + 'N5: a per-side border color emits no all-sides has-border-color class', + json_encode($sideOnlyBorderSerialized) +); +$assert( + 'border-left-color:var(--secondary);border-left-style:solid;border-left-width:2px' === $sideOnlyBorderSerialized['style'], + 'N5: a per-side border still serializes its own side declarations', + json_encode($sideOnlyBorderSerialized) +); + +$uniformBorderSerialized = $borderMapper->serialize(array( + 'border' => array( 'width' => '2px', 'style' => 'solid', 'color' => 'red' ), +)); +$assert( + 'has-border-color' === $uniformBorderSerialized['classes'], + 'N5: a uniform border color keeps the core has-border-color class', + json_encode($uniformBorderSerialized) +); + +$assert( + ! str_contains($nativeBorderGroupMarkup, 'has-border-color'), + 'N5: a Group with only an authored left border serializes without has-border-color', + $nativeBorderGroupMarkup +); + if ( $failures > 0 ) { fwrite(STDERR, "Block style support conversion tests: {$failures} failed, {$passes} passed\n"); exit(1); From f0068eb6cb82dc8087ddc25dd7aacb71b56996d2 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 19:12:30 -0700 Subject: [PATCH 4/4] fix: keep authored borders one-sided in the rendered candidate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- .../Style/StyleAttributeMapper.php | 44 +++++++++++++------ .../unit/block-style-support-conversion.php | 29 ++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php b/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php index ba4c88c0..421c3aca 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php @@ -168,10 +168,15 @@ public function serialize(array $style): array if ( '' !== trim((string) ($border['radius'] ?? '')) ) { $declarations[] = 'border-radius:' . trim((string) $border['radius']); } + // No class for a per-side color. The core style engine gives `classnames` + // to the uniform `border.color` definition only, and `has-border-color` + // is an all-sides signal: core's block-library `common.css` ships + // `html :where(.has-border-color){border-style:solid}`, which would paint + // the three unauthored sides at the initial `medium` width in + // `currentColor` and grow the box by 6px. foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { $sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array(); if ( '' !== trim((string) ($sideBorder['color'] ?? '')) ) { - $classes[] = 'has-border-color'; $declarations[] = 'border-' . $side . '-color:' . trim((string) $sideBorder['color']); } if ( '' !== trim((string) ($sideBorder['style'] ?? '')) ) { @@ -479,13 +484,13 @@ private function border(array $declarations, array &$consumed): array $noBorder = 'none' === $style || ( '' !== $width && (float) $width === 0.0 && '' === $colorValue && '' === $style ); if ( ! $noBorder ) { - if ( '' !== $width && (float) $width !== 0.0 ) { + if ( $global['width']['declared'] && '' !== $width && (float) $width !== 0.0 ) { $border['width'] = $width; } - if ( '' !== $style && 'none' !== $style ) { + if ( $global['style']['declared'] && '' !== $style && 'none' !== $style ) { $border['style'] = $style; } - if ( '' !== $colorValue ) { + if ( $global['color']['declared'] && '' !== $colorValue ) { $border['color'] = $colorValue; } } @@ -498,10 +503,12 @@ private function border(array $declarations, array &$consumed): array foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { $sideComponents = array(); + $sideDeclared = array(); foreach ( array( 'width', 'style', 'color' ) as $component ) { $candidate = $this->borderComponentCandidate($declarations, $positions, $component, $side); if ( $candidate['index'] > $global[ $component ]['index'] ) { $sideComponents[ $component ] = $candidate['value']; + $sideDeclared[ $component ] = $candidate['declared']; } } @@ -512,13 +519,13 @@ private function border(array $declarations, array &$consumed): array $sideValues = array(); $noSideBorder = 'none' === $sideStyle || ( '' !== $sideWidth && (float) $sideWidth === 0.0 && '' === $sideColor && '' === $sideStyle ); if ( ! $noSideBorder || $hasGlobalBorder ) { - if ( '' !== $sideWidth && ( (float) $sideWidth !== 0.0 || $hasGlobalBorder ) ) { + if ( ( ($sideDeclared['width'] ?? false) || $hasGlobalBorder ) && '' !== $sideWidth && ( (float) $sideWidth !== 0.0 || $hasGlobalBorder ) ) { $sideValues['width'] = $sideWidth; } - if ( '' !== $sideStyle && ( 'none' !== $sideStyle || $hasGlobalBorder ) ) { + if ( ( ($sideDeclared['style'] ?? false) || $hasGlobalBorder ) && '' !== $sideStyle && ( 'none' !== $sideStyle || $hasGlobalBorder ) ) { $sideValues['style'] = $sideStyle; } - if ( '' !== $sideColor ) { + if ( ( ($sideDeclared['color'] ?? false) || $hasGlobalBorder ) && '' !== $sideColor ) { $sideValues['color'] = $sideColor; } } @@ -535,17 +542,23 @@ private function border(array $declarations, array &$consumed): array /** * Return the last authored global or per-side value for one border component. * A shorthand participates even when it omits the component because CSS - * shorthands reset omitted values to their initial state. + * shorthands reset omitted values to their initial state. Such a substituted + * initial value is reported as `declared: false`: it settles precedence and + * cancels a border this mapper itself emits, but it is never authored, so + * callers must not serialize it. Materializing one would place an inline + * declaration the author never wrote above their own state rules — a + * `border: 2px solid transparent` base plus a `:hover { border-color }` rule + * would freeze at `currentColor`. * * @param array $declarations * @param array $positions - * @return array{value: string, index: int} + * @return array{value: string, index: int, declared: bool} */ private function borderComponentCandidate(array $declarations, array $positions, string $component, string $side = ''): array { $shorthandName = '' === $side ? 'border' : 'border-' . $side; $longhandName = $shorthandName . '-' . $component; - $candidate = array( 'value' => '', 'index' => -1 ); + $candidate = array( 'value' => '', 'index' => -1, 'declared' => false ); if ( isset($declarations[ $shorthandName ]) ) { $shorthand = $this->parseBorderShorthand($declarations[ $shorthandName ]); @@ -554,16 +567,19 @@ private function borderComponentCandidate(array $declarations, array $positions, 'style' => 'none', 'color' => 'currentColor', ); + $authored = array() !== $shorthand && isset($shorthand[ $component ]); $candidate = array( - 'value' => array() === $shorthand ? '' : (string) ($shorthand[ $component ] ?? $initialValues[ $component ]), - 'index' => $positions[ $shorthandName ], + 'value' => array() === $shorthand ? '' : (string) ($shorthand[ $component ] ?? $initialValues[ $component ]), + 'index' => $positions[ $shorthandName ], + 'declared' => $authored, ); } if ( isset($declarations[ $longhandName ]) && $positions[ $longhandName ] > $candidate['index'] ) { $candidate = array( - 'value' => $declarations[ $longhandName ], - 'index' => $positions[ $longhandName ], + 'value' => $declarations[ $longhandName ], + 'index' => $positions[ $longhandName ], + 'declared' => true, ); } diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index 889aba79..e81a26f3 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -570,6 +570,35 @@ $nativeBorderGroupMarkup ); +// A shorthand that omits a component resets it to its initial value, but that +// value is not authored. Serializing it would put an inline declaration the +// author never wrote above their own state rules: `.card{border:2px solid +// transparent}` plus `.card:hover{border-color:var(--x)}` would freeze the card +// at `currentColor`. The substituted initial value settles precedence only. +$transparentShorthandBorder = $borderMapper->map(array( 'border' => '2px solid transparent' )); +$assert( + array( 'width' => '2px', 'style' => 'solid' ) === ($transparentShorthandBorder['style']['border'] ?? array()), + 'N6: a shorthand whose color is unusable emits no substituted currentColor', + json_encode($transparentShorthandBorder) +); +$colorlessShorthandBorder = $borderMapper->map(array( 'border' => '1px solid' )); +$assert( + array( 'width' => '1px', 'style' => 'solid' ) === ($colorlessShorthandBorder['style']['border'] ?? array()), + 'N6: a shorthand that omits the color emits no substituted currentColor', + json_encode($colorlessShorthandBorder) +); +$colorlessSideShorthandBorder = $borderMapper->map(array( 'border-bottom' => '1px solid' )); +$assert( + array( 'bottom' => array( 'width' => '1px', 'style' => 'solid' ) ) === ($colorlessSideShorthandBorder['style']['border'] ?? array()), + 'N6: a per-side shorthand that omits the color emits no substituted currentColor', + json_encode($colorlessSideShorthandBorder) +); +$assert( + array( 'width' => 'medium', 'style' => 'none', 'color' => 'red' ) === ($globalThenColorOnlyLeftBorder['style']['border']['left'] ?? array()), + 'N6: substituted initial values still cancel a global border this mapper emits', + json_encode($globalThenColorOnlyLeftBorder) +); + if ( $failures > 0 ) { fwrite(STDERR, "Block style support conversion tests: {$failures} failed, {$passes} passed\n"); exit(1);