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
1 change: 1 addition & 0 deletions php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"php tests/unit/fallback-finding-normalizer.php",
"php tests/unit/navigation-underline-color-resolver.php",
"php tests/unit/button-signal-classifier.php",
"php tests/unit/button-style-resolver.php",
"php tests/unit/button-visual-probe-diagnostics.php",
"php tests/unit/block-style-support-conversion.php",
"php tests/unit/content-round-trip-reporter.php",
Expand Down
5 changes: 5 additions & 0 deletions php-transformer/src/HtmlToBlocks/BlockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,11 @@ private function buttonStyleSupport(array $attrs): array
$declarations[] = 'background-color:' . $background;
}

$shadow = trim((string) ($style['shadow'] ?? ''));
if ( '' !== $shadow ) {
$declarations[] = 'box-shadow:' . $shadow;
}

$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] ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
* - Outline/ghost buttons (transparent/absent background) get style.border.* +
* style.color.text and never a background or gradient fill.
* - Buttons carry padding but not margin (inter-button spacing rides on the
* parent core/buttons block gap), plus a curated typography subset.
* parent core/buttons block gap), plus source shadow and a curated
* typography subset.
* A button whose resolved CSS carries no paintable colors/border stays default.
*/
final class ButtonStyleResolver
Expand Down Expand Up @@ -85,6 +86,11 @@ public function nativeAttributes(string $resolvedStyle): array
$style['spacing']['padding'] = $padding;
}

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

$typography = $this->buttonTypography(is_array($mapped['typography'] ?? null) ? $mapped['typography'] : array());
if ( array() !== $typography ) {
$style['typography'] = $typography;
Expand Down Expand Up @@ -119,6 +125,27 @@ private function buttonWidth(array $declarations): ?int
return null;
}

/**
* Core/button supports shadow as a canonical `style.shadow` value. Keep this
* button-specific instead of making every block consume `box-shadow`, because
* class-owned card/section shadows should continue riding on preserved CSS.
*
* @param array<string, string> $declarations
*/
private function buttonShadow(array $declarations): string
{
$shadow = trim((string) ($declarations['box-shadow'] ?? ''));
if ( '' === $shadow || in_array(strtolower($shadow), array( 'none', 'initial', 'inherit', 'unset' ), true) ) {
return '';
}

if ( preg_match('/(?:expression\s*\(|javascript\s*:|url\s*\()/i', $shadow) ) {
return '';
}

return $shadow;
}

/**
* Project the shared typography attributes onto the button-supported subset,
* preserving the canonical emission order.
Expand Down
96 changes: 95 additions & 1 deletion php-transformer/src/VisualParity/ButtonMenuVisualProbe.php
Original file line number Diff line number Diff line change
Expand Up @@ -492,12 +492,66 @@ private function computedStyle(DOMElement $element, array $rules): array
$style = array_merge($style, $this->declarations($element->getAttribute('style')));
}

$style = $this->resolveCustomProperties($element, $style);
$style = array_intersect_key($style, array_flip(self::STYLE_FIELDS));
$style = $this->withCoreButtonWidthClass($element, $style);
ksort($style);

return $style;
}

/**
* @param array<string, string> $style
* @return array<string, string>
*/
private function resolveCustomProperties(DOMElement $element, array $style): array
{
if ( array() === $style ) {
return $style;
}

$customProperties = $this->customProperties($element->ownerDocument);
if ( array() === $customProperties ) {
return $style;
}

foreach ( $style as $property => $value ) {
if ( ! str_contains($value, 'var(') ) {
continue;
}
$style[$property] = preg_replace_callback('/var\(\s*(--[A-Za-z0-9_-]+)\s*(?:,\s*([^()]*))?\)/', static function (array $matches) use ($customProperties): string {
$name = (string) ($matches[1] ?? '');
if ( isset($customProperties[$name]) ) {
return $customProperties[$name];
}
return trim((string) ($matches[2] ?? $matches[0]));
}, $value) ?? $value;
}

return $style;
}

/**
* @return array<string, string>
*/
private function customProperties(?DOMDocument $document): array
{
if ( ! $document instanceof DOMDocument ) {
return array();
}

$properties = array();
foreach ( $document->getElementsByTagName('style') as $style ) {
if ( preg_match_all('/(--[A-Za-z0-9_-]+)\s*:\s*([^;{}]+)/', (string) $style->textContent, $matches, PREG_SET_ORDER) ) {
foreach ( $matches as $match ) {
$properties[(string) $match[1]] = trim((string) $match[2]);
}
}
}

return $properties;
}

/**
* @param array<int, array{selector: string, declarations: array<string, string>}> $rules
* @return array<string, mixed>
Expand Down Expand Up @@ -601,6 +655,9 @@ private function styleRules(DOMDocument $document): array
if ( '' === $selector ) {
continue;
}
if ( $this->selectorCarriesPseudoState($selector) ) {
continue;
}
$rules[] = array(
'selector' => $selector,
'declarations' => $this->declarations((string) $match[2]),
Expand All @@ -612,6 +669,39 @@ private function styleRules(DOMDocument $document): array
return $rules;
}

/**
* WordPress renders core/button custom width as a wrapper class. The visual
* probe compares the inner link/button control, so synthesize the equivalent
* width declaration from the nearest core/button wrapper.
*
* @param array<string, string> $style
* @return array<string, string>
*/
private function withCoreButtonWidthClass(DOMElement $element, array $style): array
{
if ( isset($style['width']) ) {
return $style;
}

for ( $node = $element; $node instanceof DOMElement; $node = $node->parentNode instanceof DOMElement ? $node->parentNode : null ) {
$className = $node->hasAttribute('class') ? $node->getAttribute('class') : '';
if ( preg_match('/(?:^|\s)wp-block-button__width-(25|50|75|100)(?:\s|$)/', $className, $match) ) {
$style['width'] = $match[1] . '%';
return $style;
}
if ( 'body' === strtolower($node->tagName) ) {
break;
}
}

return $style;
}

private function selectorCarriesPseudoState(string $selector): bool
{
return 1 === preg_match('/:{1,2}(?:hover|focus-visible|focus-within|focus|active|visited|before|after)\b/i', $selector);
}

/**
* @return array<string, string>
*/
Expand All @@ -634,7 +724,11 @@ private function declarations(string $style): array

private function matchesSimpleSelector(DOMElement $element, string $selector): bool
{
$selector = trim(preg_replace('/:(hover|focus|active|visited|before|after)\b.*/', '', $selector) ?? $selector);
if ( $this->selectorCarriesPseudoState($selector) ) {
return false;
}

$selector = trim($selector);
if ( '' === $selector || str_contains($selector, '>') || str_contains($selector, '+') || str_contains($selector, '~') ) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class ButtonMenuVisualProbeComparator
'border-top-right-radius',
),
'padding' => array('padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top'),
'shadow' => array('box-shadow'),
'text_color' => array('color'),
'width' => array('width', 'min-width'),
);
Expand Down Expand Up @@ -310,6 +311,7 @@ private function guidanceForGroup(string $group): string
'padding' => 'Preserve source padding on the button/link element or its wrapper block spacing support.',
'radius' => 'Preserve source border radius on the core/button border support.',
'fill' => 'Preserve source fill/background color and avoid theme default fill leakage.',
'shadow' => 'Preserve source box-shadow/glow on the core/button shadow support.',
'border' => 'Preserve border width/style/color together; partial border emission usually changes button shape.',
'text_color' => 'Preserve link text color separately from fill/background color.',
default => '',
Expand Down Expand Up @@ -415,6 +417,20 @@ private function groupValues(array $style, array $fields): array
private function styleGroupValues(array $style, string $group, array $fields): array
{
$values = $this->groupValues($style, $fields);
if ( 'fill' === $group ) {
$fill = trim((string) ($values['background-color'] ?? $values['background'] ?? ''));
return '' === $fill ? array() : array('background' => $fill);
}

if ( 'padding' === $group ) {
return $this->boxValues('padding', $values);
}

if ( 'radius' === $group ) {
$radius = trim((string) ($values['border-radius'] ?? ''));
return '' === $radius ? $values : array('border-radius' => $radius);
}

if ( 'border' !== $group ) {
return $values;
}
Expand All @@ -441,6 +457,39 @@ private function styleGroupValues(array $style, string $group, array $fields): a
}, ARRAY_FILTER_USE_BOTH);
}

/**
* @param array<string, string> $values
* @return array<string, string>
*/
private function boxValues(string $prefix, array $values): array
{
$shorthand = trim((string) ($values[$prefix] ?? ''));
if ( '' !== $shorthand ) {
$parts = preg_split('/\s+/', $shorthand) ?: array();
$parts = array_values(array_filter($parts, static fn (string $part): bool => '' !== $part));
if ( 1 === count($parts) ) {
$parts = array($parts[0], $parts[0], $parts[0], $parts[0]);
} elseif ( 2 === count($parts) ) {
$parts = array($parts[0], $parts[1], $parts[0], $parts[1]);
} elseif ( 3 === count($parts) ) {
$parts = array($parts[0], $parts[1], $parts[2], $parts[1]);
} else {
$parts = array_slice($parts, 0, 4);
}
$values = array(
$prefix . '-top' => $parts[0],
$prefix . '-right' => $parts[1],
$prefix . '-bottom' => $parts[2],
$prefix . '-left' => $parts[3],
) + $values;
}

unset($values[$prefix]);
ksort($values);

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

/**
* @param array<string, string> $style
* @return array<int, string>
Expand Down
49 changes: 49 additions & 0 deletions php-transformer/tests/unit/button-style-resolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

/**
* Regression coverage for native core/button style emission.
*/

require dirname(__DIR__, 2) . '/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer;

$failures = 0;
$passes = 0;

$assert = static function (bool $condition, string $message, string $detail = '') use (&$failures, &$passes): void {
if ( $condition ) {
++$passes;
return;
}

++$failures;
fwrite(STDERR, 'FAIL: ' . $message . ('' !== $detail ? ' - ' . $detail : '') . PHP_EOL);
};

$html = <<<'HTML'
<style>
.btn { padding:9px 20px; border-radius:6px; font-weight:600; text-transform:uppercase; }
.btn-primary { background:#e8a020; color:#050d1a; box-shadow:0 0 24px rgba(232,160,32,0.3); }
</style>
<a class="btn btn-primary" href="/demo">Get early access</a>
HTML;

$result = ( new HtmlTransformer() )->transform($html, array())->toArray();
$button = $result['blocks'][0]['innerBlocks'][0] ?? array();
$attrs = is_array($button['attrs'] ?? null) ? $button['attrs'] : array();
$markup = (string) ($result['serialized_blocks'] ?? '');

$assert('core/button' === ($button['blockName'] ?? ''), 'button signal becomes core/button', (string) ($button['blockName'] ?? '(none)'));
$assert('0 0 24px rgba(232,160,32,0.3)' === ($attrs['style']['shadow'] ?? ''), 'button box-shadow maps to canonical style.shadow', json_encode($attrs['style'] ?? array()));
$assert(str_contains($markup, 'box-shadow:0 0 24px rgba(232,160,32,0.3)'), 'rendered core/button carries source box-shadow', $markup);
$assert(str_contains($markup, 'background-color:#e8a020'), 'rendered core/button carries source fill', $markup);
$assert(str_contains($markup, 'color:#050d1a'), 'rendered core/button carries source text color', $markup);

if ( $failures > 0 ) {
fwrite(STDERR, "Button style resolver tests: {$failures} failed, {$passes} passed\n");
exit(1);
}

fwrite(STDOUT, "Button style resolver tests: {$passes} passed\n");
18 changes: 18 additions & 0 deletions php-transformer/tests/unit/button-visual-probe-diagnostics.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@
);
$assert(! in_array('button_border_missing', $causeCodes($zeroBorderReport), true), 'ignores non-visual border:0 declarations', json_encode($zeroBorderReport['matches'][0] ?? array()));

$hoverReport = $compare(
'<style>.cta{background:#e8a020;color:#050d1a;padding:9px 20px}.cta:hover{background:#f0ac22}</style><a class="cta" href="/demo">Start</a>',
'<style>.wp-block-button__link{background:#e8a020;color:#050d1a;padding:9px 20px}</style><a class="wp-block-button__link" href="/demo">Start</a>'
);
$assert(! in_array('button_fill_mismatch', $causeCodes($hoverReport), true), 'ignores hover fill when comparing resting button state', json_encode($hoverReport['matches'][0] ?? array()));

$widthReport = $compare(
'<style>.cta{width:100%;padding:9px 20px;background:#111;color:#fff}</style><a class="cta" href="/demo">Start</a>',
'<style>.wp-block-button__link{padding:9px 20px;background:#111;color:#fff}</style><div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link" href="/demo">Start</a></div>'
);
$assert(! in_array('button_width_missing', $causeCodes($widthReport), true), 'recognizes core/button custom width wrapper classes', json_encode($widthReport['matches'][0] ?? array()));

$shadowReport = $compare(
'<style>.cta{padding:9px 20px;background:#e8a020;color:#050d1a;box-shadow:0 0 24px rgba(232,160,32,0.3)}</style><a class="cta" href="/demo">Start</a>',
'<style>.wp-block-button__link{padding:9px 20px;background:#e8a020;color:#050d1a}</style><a class="wp-block-button__link" href="/demo">Start</a>'
);
$assert(in_array('button_shadow_missing', $causeCodes($shadowReport), true), 'reports missing button shadow/glow', json_encode($shadowReport['matches'][0] ?? array()));

if ( $failures > 0 ) {
fwrite(STDERR, "Button visual probe diagnostic tests: {$failures} failed, {$passes} passed\n");
exit(1);
Expand Down
Loading