From c97a034fff5eb293a56374d53f13451a774c2d8d Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 07:21:26 -0700 Subject: [PATCH 1/2] Test authored inline list styles are preserved --- .../unit/block-style-support-conversion.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/php-transformer/tests/unit/block-style-support-conversion.php b/php-transformer/tests/unit/block-style-support-conversion.php index 35df784b..57d91d02 100644 --- a/php-transformer/tests/unit/block-style-support-conversion.php +++ b/php-transformer/tests/unit/block-style-support-conversion.php @@ -90,6 +90,38 @@ $assert(! str_contains($nativeGridMarkup, 'gap:1.2rem'), '16c: native Group grid markup omits inline gap that Gutenberg save does not reproduce', $nativeGridMarkup); $assert(str_contains($nativeGridCss, 'gap:1.2rem !important'), '16d: inline native Group grid gap moves to the generated geometry carrier', $nativeGridCss); +$unorderedListSource = ''; +$unorderedListResult = ( new HtmlTransformer() )->transform($unorderedListSource)->toArray(); +$unorderedListAttrs = $unorderedListResult['blocks'][0]['attrs'] ?? array(); +$unorderedListCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), is_array($unorderedListResult['assets'] ?? null) ? $unorderedListResult['assets'] : array())); +$assert(str_contains($unorderedListSource, 'list-style:none'), 'L1 precondition: unordered list fixture authors list-style:none', $unorderedListSource); +$assert(str_contains((string) ($unorderedListAttrs['className'] ?? ''), 'be-inline-geometry-') && str_contains($unorderedListCss, 'list-style:none') && ! str_contains($unorderedListCss, 'list-style:none !important'), 'L1: unordered list carries authored list-style:none without !important', $unorderedListCss); + +$orderedListResult = ( new HtmlTransformer() )->transform('
  1. First
')->toArray(); +$orderedListAttrs = $orderedListResult['blocks'][0]['attrs'] ?? array(); +$orderedListCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), is_array($orderedListResult['assets'] ?? null) ? $orderedListResult['assets'] : array())); +$assert(true === ($orderedListAttrs['ordered'] ?? false) && str_contains((string) ($orderedListAttrs['className'] ?? ''), 'be-inline-geometry-') && str_contains($orderedListCss, 'list-style:none') && ! str_contains($orderedListCss, 'list-style:none !important'), 'L2: ordered list carries authored list-style:none without !important', $orderedListCss); + +$plainListResult = ( new HtmlTransformer() )->transform('')->toArray(); +$plainListMarkup = (string) ($plainListResult['serialized_blocks'] ?? ''); +$assert( + '' === $plainListMarkup + && array() === ($plainListResult['assets'] ?? array()), + 'L3: a list without authored list-style keeps canonical marker-rendering output and gains no carrier', + $plainListMarkup +); + +$listLonghandsResult = ( new HtmlTransformer() )->transform('')->toArray(); +$listLonghandsCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), is_array($listLonghandsResult['assets'] ?? null) ? $listLonghandsResult['assets'] : array())); +$assert( + str_contains($listLonghandsCss, 'list-style-type:square') + && str_contains($listLonghandsCss, 'list-style-position:inside') + && str_contains($listLonghandsCss, 'list-style-image:url(https://example.com/marker.svg)') + && ! str_contains($listLonghandsCss, '!important'), + 'list-style longhands ride the generated carrier without !important', + $listLonghandsCss +); + $cardHtml = '

Team

Scale every launch.

'; $cardResult = ( new HtmlTransformer() )->transform($cardHtml, array())->toArray(); $cardShell = $cardResult['blocks'][0] ?? array(); From 1090544933b264cec75c90d9989b3df4329ec5c8 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 07:38:30 -0700 Subject: [PATCH 2/2] Preserve authored inline list styles --- .../Style/StyleResolutionTrait.php | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php index 85ecb963..8749a241 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php @@ -77,12 +77,25 @@ private function inlineLayoutCarrierProperties(): array ); } + /** + * @return list + */ + private function inlineListMarkerCarrierProperties(): array + { + return array( + 'list-style', + 'list-style-type', + 'list-style-position', + 'list-style-image', + ); + } + /** * @return list */ private function inlineGeometryProperties(): array { - return array_merge($this->inlineLayoutCarrierProperties(), array( + return array_merge($this->inlineLayoutCarrierProperties(), $this->inlineListMarkerCarrierProperties(), array( 'width', 'height', 'min-width', @@ -375,7 +388,7 @@ private function inlineGeometryClassName( } $rawValue = trim((string) ($declarations[$property] ?? ($forcedDeclarations[$property] ?? ''))); $value = trim(preg_replace('/\s*!\s*important\s*$/i', '', $rawValue) ?? $rawValue); - if ( in_array($property, array( 'background', 'background-image' ), true) ) { + if ( in_array($property, array( 'background', 'background-image', 'list-style', 'list-style-image' ), true) ) { $value = CssUrlRewriter::rewrite($value, fn (string $url): string => $this->resolvedAssetImageUrl($url)); } if ('' !== $value && ! preg_match('~[{}<>;]|/\*~', $value)) { @@ -400,16 +413,18 @@ private function inlineGeometryClassName( // (forced/custom-property fallbacks) sort last. $sourceOrder = array_flip(array_keys($declarations)); uksort($geometry, static fn (string $a, string $b): int => (($sourceOrder[$a] ?? PHP_INT_MAX) <=> ($sourceOrder[$b] ?? PHP_INT_MAX)) ?: strcmp($a, $b)); - $layoutDeclarations = array(); + $normalPriorityDeclarations = array(); $importantDeclarations = array(); $forcedPropertyLookup = array_fill_keys($forcedProperties, true); $inlineLayoutPropertyLookup = array_fill_keys($this->inlineLayoutCarrierProperties(), true); + $inlineListMarkerPropertyLookup = array_fill_keys($this->inlineListMarkerCarrierProperties(), true); foreach ($geometry as $property => $value) { - if ( isset($inlineLayoutPropertyLookup[$property]) && ! isset($forcedPropertyLookup[$property]) ) { - // Preserve source inline layout over a later plain author class - // without preventing an authored !important declaration from - // retaining its normal cascade priority. - $layoutDeclarations[] = $property . ':' . $value; + if ( isset($inlineListMarkerPropertyLookup[$property]) + || ( isset($inlineLayoutPropertyLookup[$property]) && ! isset($forcedPropertyLookup[$property]) ) + ) { + // Preserve source inline layout and list markers over a later + // plain author class without introducing !important. + $normalPriorityDeclarations[] = $property . ':' . $value; continue; } @@ -418,11 +433,11 @@ private function inlineGeometryClassName( // rules retain their normal cascade priority through specificity. $importantDeclarations[] = $property . ':' . $value . ' !important'; } - $signature = implode(';', array_merge($layoutDeclarations, $importantDeclarations)); + $signature = implode(';', array_merge($normalPriorityDeclarations, $importantDeclarations)); $className = ($this->geometryCarrierClassAllocator ??= new GeometryCarrierClassAllocator())->allocate($this->geometryStructuralPath($element) . "\n" . $signature); $rules = array(); - if ( array() !== $layoutDeclarations ) { - $rules[] = ':root .' . $className . '{' . implode(';', $layoutDeclarations) . '}'; + if ( array() !== $normalPriorityDeclarations ) { + $rules[] = ':root .' . $className . '{' . implode(';', $normalPriorityDeclarations) . '}'; } if ( array() !== $importantDeclarations ) { $rules[] = '.' . $className . '{' . implode(';', $importantDeclarations) . '}'; @@ -1758,7 +1773,7 @@ private function cssDeclarations(string $style): array [$name, $value] = array_map('trim', explode(':', $declaration, 2)); $name = strtolower($name); $value = preg_replace('/\s+/', ' ', $value) ?? $value; - $allowsImageUrl = in_array($name, array( 'background', 'background-image' ), true) && ! preg_match('/(?:expression\s*\(|javascript\s*:)/i', $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) ) ) { $declarations[$name] = $value; }