diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index 3f160525..5553e608 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -62,6 +62,8 @@ public function create(string $name, array $attrs = array(), array $innerBlocks */ private function normalizeAttrsForBlock(string $name, array $attrs): array { + $attrs = $this->normalizeClassNameAttr($attrs); + if ( in_array($name, array( 'core/group', 'core/columns' ), true) ) { unset($attrs['style']['dimensions']['maxWidth']); if ( empty($attrs['style']['dimensions']) ) { @@ -85,6 +87,33 @@ private function normalizeAttrsForBlock(string $name, array $attrs): array return $attrs; } + /** + * @param array $attrs + * @return array + */ + private function normalizeClassNameAttr(array $attrs): array + { + if ( ! is_string($attrs['className'] ?? null) ) { + return $attrs; + } + + $classes = array(); + foreach ( preg_split('/\s+/', trim($attrs['className'])) ?: array() as $class ) { + if ( '' === $class || GeneratedGutenbergClassPolicy::isGeneratedClassName($class) || in_array($class, $classes, true) ) { + continue; + } + $classes[] = $class; + } + + if ( array() === $classes ) { + unset($attrs['className']); + return $attrs; + } + + $attrs['className'] = implode(' ', $classes); + return $attrs; + } + /** * @param array $attrs * @return array diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 356faebb..c8d3a4b8 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -804,6 +804,24 @@ public function match(DOMElement $element, PatternContext $context): ?array $assert(str_contains($canonicalWrapperAttrsSerialized, '

Menu

'), 'heading wrappers include canonical and support classes with supported color style'); $assert(str_contains($canonicalWrapperAttrsSerialized, '

Fresh daily.

'), 'paragraph wrappers preserve runtime-addressable classes and supported margin style'); +$paragraphGeneratedClassLeakResult = ( new HtmlTransformer() )->transform( + '

Slow roasted daily.

' +)->toArray(); +$paragraphGeneratedClassLeakBlock = $paragraphGeneratedClassLeakResult['blocks'][0] ?? array(); +$paragraphGeneratedClassLeakSerialized = (string) ($paragraphGeneratedClassLeakResult['serialized_blocks'] ?? ''); +$assert('hero-tagline' === ($paragraphGeneratedClassLeakBlock['attrs']['className'] ?? ''), 'paragraph className strips duplicate generated color classes while preserving custom classes'); +$assert(str_contains($paragraphGeneratedClassLeakSerialized, '

Slow roasted daily.

'), 'paragraph serialization emits generated has-text-color once before custom classes'); +$assert(1 === substr_count($paragraphGeneratedClassLeakSerialized, 'has-text-color'), 'paragraph serialization emits has-text-color exactly once'); +$assert('pass' === ($paragraphGeneratedClassLeakResult['source_reports']['wp_block_validity']['status'] ?? ''), 'paragraph with stripped generated className passes generated block validity checks'); + +$paragraphFactoryGeneratedClassLeakBlock = ( new BlockFactory() )->create('core/paragraph', array( + 'content' => 'Slow roasted daily.', + 'className' => 'has-text-color has-text-color hero-tagline', + 'style' => array('color' => array('text' => 'red')), +)); +$assert('hero-tagline' === ($paragraphFactoryGeneratedClassLeakBlock['attrs']['className'] ?? ''), 'BlockFactory strips generated classes from direct paragraph className attrs'); +$assert(str_contains((string) ($paragraphFactoryGeneratedClassLeakBlock['innerHTML'] ?? ''), '

Slow roasted daily.

'), 'BlockFactory paragraph innerHTML emits a single generated color class'); + $paragraphSvgResult = ( new HtmlTransformer() )->transform( '
' )->toArray();