From b8f52d3d33e2844fd1ea8528793e9abffbf7fa62 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 23 Jul 2026 07:44:00 -0400 Subject: [PATCH] Collapse typography-only text wrappers to styled paragraphs A styled `
`/`` whose only content is inline text and whose CSS is typographic (no box-model geometry) was round-tripped as a `core/group` wrapping a default `core/paragraph`. That form does not inherit the wrapper's typographic scale onto the inner paragraph and does not suppress default block spacing, so an eyebrow like `
The Shop
` rendered at the wrong font size and introduced a ~21px top offset that shifted every following block down the page (visible across imported artist-site routes). visualTextWrapperBlockFromElement now emits a single styled `core/paragraph` carrying the wrapper class when the wrapper is a pure-text leaf with no box-model styling (padding/border/explicit sizing/flex-grid). Wrappers that own box-model geometry (`.tag`, `.tier-price`, `.use-case-result`) still round-trip as `core/group` so their block-level layout is preserved. Box-model detection reads the raw matched declarations via structuralPresentationDeclarations so padding consumed into block-supports attributes is still seen. Updates block-style-support conversion tests: box-model wrappers stay groups; the typography-only `.tier-name` label now collapses to a styled paragraph. Full php-transformer suite green: 248 parity fixtures, 75 block-style-support tests, wordpress-site-plan + shared-shell-plan contracts. Refs #630. AI assistance: drafted with Claude (Sonnet 4.5) via opencode; DOM-snapshot root-cause and implementation reviewed by Chris Huber. --- .../src/HtmlToBlocks/HtmlTransformer.php | 39 ++++++++++++++++++- .../unit/block-style-support-conversion.php | 10 ++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 19c6ac27..eeca1d72 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -3740,6 +3740,24 @@ private function visualTextWrapperBlockFromElement(DOMElement $element): ?array return null; } + // A pure-text styled wrapper whose CSS is typographic only (no box-model + // geometry) round-trips as a single styled `core/paragraph` carrying the + // wrapper class. The `core/group` + default inner paragraph form neither + // inherits the wrapper's typographic scale onto that inner paragraph nor + // suppresses default block spacing, so an eyebrow like `
+ // The Shop
` renders at the wrong size and pushes every following + // block down. The group form is retained only when the wrapper owns + // box-model geometry (padding/border/flex) that a block-level container + // must preserve. + if ( 0 === $this->childElementCount($element) && ! $this->hasBoxModelWrapperStyling($element) ) { + return $this->createBlock( + 'core/paragraph', + array_merge($this->presentationAttributes($element), array( 'content' => $content )), + array(), + $element + ); + } + return $this->createBlock( 'core/group', $this->presentationAttributes($element), @@ -3748,6 +3766,15 @@ private function visualTextWrapperBlockFromElement(DOMElement $element): ?array ); } + /** + * Box-model CSS declarations that give a text wrapper block-level geometry + * (padding, border, explicit sizing, or flex/grid layout) which must be + * preserved on a `core/group` rather than flattened onto a paragraph. + * + * @var array + */ + private const BOX_MODEL_WRAPPER_PROPERTIES = array( 'display', 'gap', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border', 'border-color', 'border-radius', 'width', 'height', 'min-width', 'max-width', 'min-height' ); + private function hasVisualTextWrapperSignal(DOMElement $element): bool { $className = strtolower($this->attr($element, 'class')); @@ -3759,8 +3786,16 @@ private function hasVisualTextWrapperSignal(DOMElement $element): bool return false; } - $declarations = $this->presentationDeclarations($element); - foreach ( array( 'display', 'gap', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border', 'border-color', 'border-radius', 'width', 'height', 'min-width', 'max-width', 'min-height' ) as $property ) { + return $this->hasBoxModelWrapperStyling($element); + } + + private function hasBoxModelWrapperStyling(DOMElement $element): bool + { + // Read the raw matched declarations rather than the post-projection + // presentation set: box-model properties such as padding are consumed + // into block-supports attributes and would otherwise be invisible here. + $declarations = $this->structuralPresentationDeclarations($element); + foreach ( self::BOX_MODEL_WRAPPER_PROPERTIES as $property ) { if ( isset($declarations[$property]) && '' !== trim((string) $declarations[$property]) ) { return true; } diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index b6bc2ab3..dca52009 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -187,11 +187,11 @@ $labelResult = ( new HtmlTransformer() )->transform($labelHtml, array('static_css' => $labelCss))->toArray(); $labelMarkup = (string) ($labelResult['serialized_blocks'] ?? ''); -$assert(str_contains($labelMarkup, '
Team

'), '26: typography-only card tier label collapses to a styled paragraph so its font scale applies', $labelMarkup); +$assert(str_contains($labelMarkup, '
]*"className":"tier-name"/', $labelMarkup), '29: typography-only tier label does not round-trip as a group wrapping a default paragraph', $labelMarkup); $stackHtml = '

Eyebrow

Low Tide Table

Local shrimp.

Next Run

'; $stackResult = ( new HtmlTransformer() )->transform($stackHtml, array())->toArray();