Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 80 additions & 4 deletions php-transformer/src/HtmlToBlocks/BlockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ private function blockHtml(string $name, array $attrs, array $innerBlocks): stri
}

if ( 'core/button' === $name ) {
$support = $this->buttonStyleSupport($attrs);

if ( 'button' === ($attrs['tagName'] ?? '') ) {
$buttonAttrs = array_intersect_key($attrs, array_flip(array( 'type', 'role', 'aria-label', 'aria-controls', 'aria-expanded', 'aria-haspopup' )));
foreach ( $attrs as $attrName => $attrValue ) {
Expand All @@ -211,8 +213,8 @@ private function blockHtml(string $name, array $attrs, array $innerBlocks): stri
}
$buttonAttrs = array_merge(array(
'id' => (string) ($attrs['anchor'] ?? ''),
'class' => $this->mergeClassNames('wp-block-button__link wp-element-button', (string) ($attrs['className'] ?? '')),
'style' => (string) ($attrs['style'] ?? ''),
'class' => $this->mergeClassNames('wp-block-button__link', $support['classes'], 'wp-element-button', (string) ($attrs['className'] ?? '')),
'style' => $support['style'],
), $buttonAttrs);

return '<div class="wp-block-button"><button' . $this->htmlAttrs($buttonAttrs) . '>' . ($attrs['text'] ?? '') . '</button></div>';
Expand All @@ -221,8 +223,8 @@ private function blockHtml(string $name, array $attrs, array $innerBlocks): stri
$href = '' !== ($attrs['url'] ?? '') ? ' href="' . htmlspecialchars((string) $attrs['url'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '"' : '';
$wrapperClass = $this->mergeClassNames('wp-block-button', in_array('is-style-outline', preg_split('/\s+/', (string) ($attrs['className'] ?? '')) ?: array(), true) ? 'is-style-outline' : '');
$linkAttrs = array(
'class' => $this->mergeClassNames('wp-block-button__link wp-element-button', (string) ($attrs['className'] ?? '')),
'style' => (string) ($attrs['style'] ?? ''),
'class' => $this->mergeClassNames('wp-block-button__link', $support['classes'], 'wp-element-button', (string) ($attrs['className'] ?? '')),
'style' => $support['style'],
);
return '<div class="' . htmlspecialchars($wrapperClass, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '"><a' . $this->htmlAttrs($linkAttrs) . $href . '>' . ($attrs['text'] ?? '') . '</a></div>';
}
Expand Down Expand Up @@ -404,6 +406,80 @@ private function searchHtml(array $attrs): string
return '<form role="search" method="get"' . $this->blockSupportAttrs($attrs, 'wp-block-search') . '><label' . $this->htmlAttrs($labelAttrs) . '>' . $label . '</label><div class="wp-block-search__inside-wrapper"><input' . $this->htmlAttrs($inputAttrs) . ' />' . $button . '</div></form>';
}

/**
* Translate a core/button block's native style support into the rendered
* has-* support classes and the inline style string WordPress emits for
* custom colors and borders. Accepts the canonical `style` object; falls back
* to a legacy raw `style` string when present for backward compatibility.
*
* @param array<string, mixed> $attrs
* @return array{classes: string, style: string}
*/
private function buttonStyleSupport(array $attrs): array
{
$style = $attrs['style'] ?? null;
if ( ! is_array($style) ) {
return array(
'classes' => '',
'style' => is_string($style) ? $style : '',
);
}

$classes = array();
$declarations = array();

$background = (string) ($style['color']['background'] ?? '');
$text = (string) ($style['color']['text'] ?? '');
if ( '' !== $text ) {
$classes[] = 'has-text-color';
$declarations[] = 'color:' . $text;
}
if ( '' !== $background ) {
$classes[] = 'has-background';
$declarations[] = 'background-color:' . $background;
}

$border = is_array($style['border'] ?? null) ? $style['border'] : array();
if ( isset($border['color']) && '' !== (string) $border['color'] ) {
$classes[] = 'has-border-color';
$declarations[] = 'border-color:' . (string) $border['color'];
}
if ( isset($border['width']) && '' !== (string) $border['width'] ) {
$declarations[] = 'border-width:' . (string) $border['width'];
}
if ( isset($border['style']) && '' !== (string) $border['style'] ) {
$declarations[] = 'border-style:' . (string) $border['style'];
}
if ( isset($border['radius']) && '' !== (string) $border['radius'] ) {
$declarations[] = 'border-radius:' . (string) $border['radius'];
}

$padding = is_array($style['spacing']['padding'] ?? null) ? $style['spacing']['padding'] : array();
foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
if ( isset($padding[$side]) && '' !== (string) $padding[$side] ) {
$declarations[] = 'padding-' . $side . ':' . (string) $padding[$side];
}
}

$typography = is_array($style['typography'] ?? null) ? $style['typography'] : array();
$typographyMap = array(
'fontSize' => 'font-size',
'fontWeight' => 'font-weight',
'lineHeight' => 'line-height',
'textTransform' => 'text-transform',
);
foreach ( $typographyMap as $attrName => $cssName ) {
if ( isset($typography[$attrName]) && '' !== (string) $typography[$attrName] ) {
$declarations[] = $cssName . ':' . (string) $typography[$attrName];
}
}

return array(
'classes' => implode(' ', $classes),
'style' => implode(';', $declarations),
);
}

private function mergeClassNames(string ...$classNames): string
{
$classes = array();
Expand Down
2 changes: 1 addition & 1 deletion php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3182,7 +3182,7 @@ private function interactiveAttributes(DOMElement $element): array
private function structureSignals(DOMElement $element, array $attrs): array
{
$className = strtolower(trim($this->attr($element, 'class') . ' ' . (string) ($attrs['className'] ?? '')));
$style = strtolower(trim($this->attr($element, 'style') . ';' . (string) ($attrs['style'] ?? '')));
$style = strtolower(trim($this->attr($element, 'style') . ';' . (is_string($attrs['style'] ?? null) ? $attrs['style'] : '')));
$signals = array();

if ( preg_match('/(?:^|[\s_-])(?:card|feature|service|provider|resource|post|project|stat|badge|tile|panel|item)(?:$|[\s_-])/', $className) || 'article' === strtolower($element->tagName) ) {
Expand Down
269 changes: 269 additions & 0 deletions php-transformer/src/HtmlToBlocks/Patterns/ButtonStyleResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Patterns;

/**
* Translates a button's resolved CSS declarations (from inline style plus the
* `<style>`/linked CSS rules the transformer already matches to the element)
* into native WordPress core/button block attributes.
*
* This keeps imported buttons rendering with their source colors and borders
* instead of falling back to the theme's default (grey) button styling, because
* the styling lives in canonical block attributes (style.color.*, style.border.*)
* rather than a non-canonical inline style string that WordPress drops on
* block recovery.
*
* The translation is theme-independent and keys only off the resolved
* declarations:
* - Filled buttons get style.color.background + style.color.text (+ border radius).
* - Outline/ghost buttons (transparent background) get style.border.* + style.color.text
* and never a background.
* A button whose resolved CSS carries no paintable colors/border stays default.
*/
final class ButtonStyleResolver
{
/**
* Build native core/button style attributes from a resolved CSS string.
*
* @return array<string, mixed> Either an empty array (no native styling) or
* an array with a `style` object suitable for the
* core/button block attributes.
*/
public function nativeAttributes(string $resolvedStyle): array
{
$declarations = $this->declarations($resolvedStyle);
if ( array() === $declarations ) {
return array();
}

$style = array();

$background = $this->backgroundColor($declarations);
$text = $this->color($declarations['color'] ?? '');
if ( '' !== $background ) {
$style['color']['background'] = $background;
}
if ( '' !== $text ) {
$style['color']['text'] = $text;
}

$border = $this->border($declarations);
if ( array() !== $border ) {
$style['border'] = $border;
}

$padding = $this->padding($declarations);
if ( array() !== $padding ) {
$style['spacing']['padding'] = $padding;
}

$typography = $this->typography($declarations);
if ( array() !== $typography ) {
$style['typography'] = $typography;
}

if ( array() === $style ) {
return array();
}

return array( 'style' => $style );
}

/**
* Resolve the background color, ignoring transparent/none/gradient/image
* backgrounds so outline/ghost buttons never receive a paintable fill.
*
* @param array<string, string> $declarations
*/
private function backgroundColor(array $declarations): string
{
$value = trim((string) ($declarations['background-color'] ?? ''));
if ( '' === $value ) {
// `background` shorthand: only treat it as a fill when it is a bare color.
$value = trim((string) ($declarations['background'] ?? ''));
if ( '' === $value || preg_match('/\b(?:url\s*\(|gradient\s*\()/i', $value) ) {
return '';
}
// Take the first token of the shorthand as the candidate color.
$value = preg_split('/\s+/', $value)[0] ?? '';
}

return $this->color($value);
}

/**
* @param array<string, string> $declarations
* @return array<string, string>
*/
private function border(array $declarations): array
{
$border = array();

// Longhand declarations win over the shorthand.
$shorthand = $this->parseBorderShorthand((string) ($declarations['border'] ?? ''));

$width = trim((string) ($declarations['border-width'] ?? $shorthand['width'] ?? ''));
$style = strtolower(trim((string) ($declarations['border-style'] ?? $shorthand['style'] ?? '')));
$color = $this->color((string) ($declarations['border-color'] ?? $shorthand['color'] ?? ''));

// `border: 0` / `border: none` means no border at all.
$noBorder = 'none' === $style || ( '' !== $width && (float) $width === 0.0 && '' === $color && '' === $style );
if ( ! $noBorder ) {
if ( '' !== $width && (float) $width !== 0.0 ) {
$border['width'] = $width;
}
if ( '' !== $style && 'none' !== $style ) {
$border['style'] = $style;
}
if ( '' !== $color ) {
$border['color'] = $color;
}
}

$radius = trim((string) ($declarations['border-radius'] ?? ''));
if ( '' !== $radius ) {
$border['radius'] = $radius;
}

return $border;
}

/**
* @return array{width?: string, style?: string, color?: string}
*/
private function parseBorderShorthand(string $value): array
{
$value = trim($value);
if ( '' === $value ) {
return array();
}

$parsed = array();
foreach ( preg_split('/\s+/', $value) ?: array() as $token ) {
$lower = strtolower($token);
if ( in_array($lower, array( 'none', 'hidden', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset' ), true) ) {
$parsed['style'] = $lower;
continue;
}
if ( preg_match('/^[0-9.]+(?:px|em|rem|%|pt|vw|vh)?$/i', $token) || in_array($lower, array( 'thin', 'medium', 'thick' ), true) ) {
$parsed['width'] = $token;
continue;
}
if ( '' !== $this->color($token) ) {
$parsed['color'] = $token;
}
}

return $parsed;
}

/**
* @param array<string, string> $declarations
* @return array<string, string>
*/
private function padding(array $declarations): array
{
$shorthand = trim((string) ($declarations['padding'] ?? ''));
$sides = array( 'top' => '', 'right' => '', 'bottom' => '', 'left' => '' );

if ( '' !== $shorthand ) {
$parts = preg_split('/\s+/', $shorthand) ?: array();
$count = count($parts);
if ( 1 === $count ) {
$sides = array( 'top' => $parts[0], 'right' => $parts[0], 'bottom' => $parts[0], 'left' => $parts[0] );
} elseif ( 2 === $count ) {
$sides = array( 'top' => $parts[0], 'right' => $parts[1], 'bottom' => $parts[0], 'left' => $parts[1] );
} elseif ( 3 === $count ) {
$sides = array( 'top' => $parts[0], 'right' => $parts[1], 'bottom' => $parts[2], 'left' => $parts[1] );
} elseif ( $count >= 4 ) {
$sides = array( 'top' => $parts[0], 'right' => $parts[1], 'bottom' => $parts[2], 'left' => $parts[3] );
}
}

foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
$longhand = trim((string) ($declarations[ 'padding-' . $side ] ?? ''));
if ( '' !== $longhand ) {
$sides[ $side ] = $longhand;
}
}

return array_filter($sides, static fn (string $value): bool => '' !== trim($value));
}

/**
* @param array<string, string> $declarations
* @return array<string, string>
*/
private function typography(array $declarations): array
{
$typography = array();
$map = array(
'font-size' => 'fontSize',
'font-weight' => 'fontWeight',
'line-height' => 'lineHeight',
'text-transform' => 'textTransform',
);

foreach ( $map as $cssName => $attrName ) {
$value = trim((string) ($declarations[ $cssName ] ?? ''));
if ( '' !== $value ) {
$typography[ $attrName ] = $value;
}
}

return $typography;
}

/**
* Return the value when it is a usable CSS color, otherwise an empty string.
*/
private function color(string $value): string
{
$value = trim($value);
if ( '' === $value ) {
return '';
}

$lower = strtolower($value);
if ( in_array($lower, array( 'transparent', 'none', 'inherit', 'initial', 'unset', 'revert', 'auto' ), true) ) {
return '';
}

if ( preg_match('/^#[0-9a-f]{3,8}$/i', $value) ) {
return $value;
}
if ( preg_match('/^(?:rgb|rgba|hsl|hsla)\s*\(/i', $value) ) {
return $value;
}
if ( 'currentcolor' === $lower ) {
return 'currentColor';
}
// Named colors (e.g. white, navy). Reject anything with whitespace/symbols.
if ( preg_match('/^[a-z]+$/', $lower) ) {
return $value;
}

return '';
}

/**
* @return array<string, string>
*/
private function declarations(string $style): array
{
$declarations = array();
foreach ( explode(';', $style) as $declaration ) {
if ( ! str_contains($declaration, ':') ) {
continue;
}
[ $name, $value ] = array_map('trim', explode(':', $declaration, 2));
$name = strtolower($name);
if ( '' !== $name && '' !== $value ) {
$declarations[ $name ] = preg_replace('/\s+/', ' ', $value) ?? $value;
}
}

return $declarations;
}
}
Loading
Loading