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
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ private function marginSelectorPrelude(string $prelude, AuthorStylesheetProjecti
private function rewriteStyleRule(string $prelude, string $body, AuthorStylesheetProjectionContext $context): string
{
$projectedPrelude = $this->rewriteSelectorPrelude($prelude, $context);
$body = $this->buttonLinkCompatDeclarations($prelude, $projectedPrelude, $body, $context);
$wrapperPrelude = $this->buttonPresentationWrapperPrelude($prelude, $context);
if ( '' === $wrapperPrelude ) {
$directWrapperPrelude = $this->directButtonGeometryWrapperPrelude($prelude, $context);
Expand All @@ -139,6 +140,67 @@ private function rewriteStyleRule(string $prelude, string $body, AuthorStyleshee
return $this->withButtonWrapperInnerFill($wrapperPrelude, $layout, $projectedPrelude . '{' . $control . '}');
}

/**
* core/button defaults and support styles are emitted after carried author CSS.
* Keep source button declarations authoritative after their selector is lowered
* to a generated marker, including media-query overrides.
*/
private function buttonLinkCompatDeclarations(string $prelude, string $projectedPrelude, string $body, AuthorStylesheetProjectionContext $context): string
{
if ( ! str_contains($projectedPrelude, '.wp-block-button__link') || ! $this->projectsAnchorButtonControl($prelude, $context) ) {
return $body;
}

$declarations = array();
foreach ( CssValueSplitter::splitTopLevel($body, array( ';' )) as $declaration ) {
$colon = strpos($declaration, ':');
if ( false === $colon ) {
$declarations[] = $declaration;
continue;
}
$name = trim(substr($declaration, 0, $colon));
$value = trim(substr($declaration, $colon + 1));
if ( '' === $name || '' === $value || ! $this->isButtonLinkLayoutProperty($name) || preg_match('/\s*!important\s*$/i', $value) ) {
$declarations[] = $declaration;
continue;
}
$declarations[] = $name . ':' . $value . '!important';
}
return implode(';', $declarations);
}

private function projectsAnchorButtonControl(string $prelude, AuthorStylesheetProjectionContext $context): bool
{
foreach ( CssStylesheetTransformer::splitSelectorList($prelude) ?? array() as $selector ) {
$parsed = $context->sourceStyles->parsedSelector($selector);
if ( ! $parsed['supported'] ) {
continue;
}
foreach ( $this->matchingSourceElements($selector, $parsed, $context) as $element ) {
if ( 'a' === strtolower($element->tagName)
&& '' !== $context->selectorProjections->controlMarker($element->getNodePath() ?? '') ) {
return true;
}
}
}
return false;
}

private function isButtonLinkLayoutProperty(string $property): bool
{
return 'display' === $property
|| 'gap' === $property
|| str_starts_with($property, 'flex-')
|| str_starts_with($property, 'align-')
|| str_starts_with($property, 'justify-')
|| 'width' === $property
|| 'height' === $property
|| str_starts_with($property, 'min-')
|| str_starts_with($property, 'max-')
|| 'padding' === $property
|| str_starts_with($property, 'padding-');
}

private function withoutCollapsedButtonProjectedWidths(string $prelude, string $body): ?string
{
$selectors = CssStylesheetTransformer::splitSelectorList($prelude);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ public function registerNativeButtonStyleRule(
$wrapperDeclarations = array();
$outerWrapperDeclarations = array();
$intrinsicWrapperDeclarations = array();
$responsiveAuthoredProperties = $sourceControl instanceof DOMElement
? $this->responsiveAuthoredProperties($sourceControl)
: array();
foreach ( array(
'background-color' => $style['color']['background'] ?? '',
'color' => $style['color']['text'] ?? '',
Expand All @@ -191,6 +194,9 @@ public function registerNativeButtonStyleRule(
'padding-bottom' => $style['spacing']['padding']['bottom'] ?? '',
'padding-left' => $style['spacing']['padding']['left'] ?? '',
) as $property => $value ) {
if ( isset($responsiveAuthoredProperties[$property]) ) {
continue;
}
$value = trim((string) $value);
if ( '' !== $value && ! preg_match('/[{}<>;]/', $value) ) {
$declarations[] = $property . ':' . $value . '!important';
Expand Down Expand Up @@ -277,6 +283,54 @@ public function registerNativeButtonStyleRule(
$generatedStyles->registerNativeButton($marker, $outerWrapperRule . $wrapperRule . $intrinsicWrapperRule . '.' . $marker . '.' . $marker . '>.wp-block-button__link{' . implode(';', $declarations) . '}');
}

/**
* Block support values are resolved before responsive author CSS is replayed.
* Do not let the resolved value override a later authored breakpoint.
*
* @return array<string, true>
*/
private function responsiveAuthoredProperties(DOMElement $sourceControl): array
{
$propertySources = array(
'background-color' => array( 'background', 'background-color' ),
'color' => array( 'color' ),
'border-color' => array( 'border', 'border-color' ),
'border-style' => array( 'border', 'border-style' ),
'border-width' => array( 'border', 'border-width' ),
'border-radius' => array( 'border-radius' ),
'font-size' => array( 'font-size' ),
'font-weight' => array( 'font-weight' ),
'letter-spacing' => array( 'letter-spacing' ),
'line-height' => array( 'line-height' ),
'text-transform' => array( 'text-transform' ),
'padding-top' => array( 'padding', 'padding-top' ),
'padding-right' => array( 'padding', 'padding-right' ),
'padding-bottom' => array( 'padding', 'padding-bottom' ),
'padding-left' => array( 'padding', 'padding-left' ),
);
$properties = array_values(array_unique(array_merge(...array_values($propertySources))));
$declared = $this->styleResolver->authorDeclaredPropertyValues(
$sourceControl,
$properties
);
$conditional = $this->styleResolver->conditionalAuthorDeclaredPropertyValues($sourceControl, $properties);
$responsive = array();
foreach ( $propertySources as $property => $sources ) {
$values = array();
$hasConditionalDeclaration = false;
foreach ( $sources as $source ) {
foreach ( $declared[$source] ?? array() as $value ) {
$values[CssValueInspector::comparable($value)] = true;
}
$hasConditionalDeclaration = $hasConditionalDeclaration || isset($conditional[$source]);
}
if ( $hasConditionalDeclaration && count($values) > 1 ) {
$responsive[$property] = true;
}
}
return $responsive;
}

public function registerDirectFlexButton(string $marker, DOMElement $control, GeneratedSupportStylesheetState $generatedStyles): void
{
$parent = $control->parentNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private function projectButtonAttributes(
if ( $facts->isDirectChildOfAuthorFlexLayout ) {
$this->generatedStyleProjector->registerDirectFlexButton($controlMarker, $logicalControl, $context->generatedStyles);
}
self::registerButtonWidth($attrs, $controlMarker, $context, $this->generatedStyleProjector);
$this->registerButtonWidth($attrs, $controlMarker, $logicalControl, $context);
}
}
if ( '' !== $controlMarker && '' !== $presentationPath && $presentationPath !== $logicalControlPath ) {
Expand All @@ -190,7 +190,7 @@ private function projectButtonAttributes(
: 'blocks-engine-native-button-alignment-' . $nativeButtonTextAlignment;
$attrs['className'] = SourceDom::mergeClassNames((string) ($attrs['className'] ?? ''), $nativeButtonMarker);
$this->generatedStyleProjector->registerNativeButtonStyleRule($nativeButtonMarker, $hasNativeButtonColor ? $attrs : array(), $context->generatedStyles, $nativeButtonTextAlignment);
self::registerButtonWidth($attrs, $nativeButtonMarker, $context, $this->generatedStyleProjector);
$this->registerButtonWidth($attrs, $nativeButtonMarker, $logicalControl, $context);
}
return $attrs;
}
Expand Down Expand Up @@ -261,12 +261,20 @@ private function buttonLabelHasAuthoredColor(
}

/** @param array<string, mixed> $attrs */
private static function registerButtonWidth(array $attrs, string $marker, SourceBlockAttributeProjectionContext $context, GeneratedBlockStyleProjector $generatedStyleProjector): void
private function registerButtonWidth(array $attrs, string $marker, DOMElement $sourceControl, SourceBlockAttributeProjectionContext $context): void
{
$buttonWidth = (int) ($attrs['width'] ?? 0);
if ( in_array($buttonWidth, array( 25, 50, 75, 100 ), true) ) {
$generatedStyleProjector->registerButtonWidth($marker, $buttonWidth, $context->generatedStyles);
if ( ! in_array($buttonWidth, array( 25, 50, 75, 100 ), true) ) {
return;
}
if ( 100 === $buttonWidth ) {
foreach ( $this->styleResolver->authorDeclaredPropertyValues($sourceControl, array( 'width' ))['width'] ?? array() as $value ) {
if ( '100%' !== CssValueInspector::comparable($value) ) {
return;
}
}
}
$this->generatedStyleProjector->registerButtonWidth($marker, $buttonWidth, $context->generatedStyles);
}

/** @param array<int, array<string, mixed>> $innerBlocks */
Expand Down
24 changes: 24 additions & 0 deletions php-transformer/src/HtmlToBlocks/Style/StyleResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,30 @@ public function authorDeclaredPropertyValues(DOMElement $element, array $propert
return $declared;
}

/**
* Author-declared values from conditional rules, such as media queries.
*
* @param array<int, string> $properties
* @return array<string, array<int, string>>
*/
public function conditionalAuthorDeclaredPropertyValues(DOMElement $element, array $properties): array
{
sort($properties, SORT_STRING);
$wanted = array_fill_keys($properties, true);
$declared = array();
foreach ( $this->styleRuleCandidates($element, 'conditional') as $rule ) {
if ( ! $this->matchesCssSelector($element, $rule['selector']) ) {
continue;
}
foreach ( $rule['declarations'] as $property => $value ) {
if ( isset($wanted[ strtolower((string) $property) ]) ) {
$declared[ strtolower((string) $property) ][] = $this->context->cssComparableValue((string) $value);
}
}
}
return $declared;
}

/**
* One `text-align` declaration on a container carrier restores its whole
* subtree, which is the source's own inheritance semantics: it covers block
Expand Down
14 changes: 13 additions & 1 deletion php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter
$flexAnchorButtonCss = implode("\n", array_map(static fn (array $asset): string => 'css' === ($asset['kind'] ?? '') ? (string) ($asset['content'] ?? '') : '', $flexAnchorButton['assets'] ?? array()));
$assert(str_contains((string) ($flexAnchorButtonAttrs['className'] ?? ''), 'blocks-engine-control-') && ! str_contains((string) ($flexAnchorButtonAttrs['className'] ?? ''), 'product-row'), 'styled anchor button uses a generated control marker instead of its source anchor class');
$assert(! str_contains($flexAnchorButtonMarkup, 'wp-block-button product-row') && ! str_contains($flexAnchorButtonMarkup, 'wp-element-button product-row'), 'styled anchor button keeps source anchor classes out of canonical core/button markup');
$assert(str_contains($flexAnchorButtonCss, '> :where(.wp-block-button__link){display:flex;align-items:center;gap:1rem') && str_contains($flexAnchorButtonCss, 'blocks-engine-richtext-marker') && str_contains($flexAnchorButtonCss, '{flex:1}'), 'styled anchor root and descendant selectors project through the generated marker after lowering');
$assert(str_contains($flexAnchorButtonCss, '> :where(.wp-block-button__link){display:flex!important;align-items:center!important;gap:1rem!important') && str_contains($flexAnchorButtonCss, 'blocks-engine-richtext-marker') && str_contains($flexAnchorButtonCss, '{flex:1}'), 'styled anchor root and descendant selectors project through the generated marker with priority over core button defaults');
$assert(str_contains($flexAnchorButtonMarkup, 'class="product-row__name"'), 'styled anchor button preserves descendant classes in its RichText content');
$assert('pass' === ($flexAnchorButton['source_reports']['wp_block_validity']['status'] ?? ''), 'styled anchor button remains editor-valid with marker-projected source selectors');

Expand Down Expand Up @@ -1429,6 +1429,18 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter
$assert(str_contains($fullWidthNativeButtonCss, '.wp-block-buttons){display:block!important;gap:0!important;width:100%!important}') && str_contains($fullWidthNativeButtonCss, '.wp-block-button__link){box-sizing:border-box;width:100%!important}'), 'styled full-width native button projects root geometry through the wrapper chain without overriding source wrapper margins');
$assert('pass' === ($fullWidthNativeButton['source_reports']['wp_block_validity']['status'] ?? ''), 'styled full-width native button wrapper chain remains editor-valid');

$responsiveFullWidthButton = ( new HtmlTransformer() )->transform(
'<style>.row{display:flex}.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 20px;background:#fc0}@media(min-width:992px){.btn{width:auto;padding:8px 16px}}</style><main><div class="row"><a class="btn" href="/signup"><svg viewBox="0 0 24 24"><path d="M0 0"/></svg><span>Sign Up</span></a></div></main>'
)->toArray();
$responsiveFullWidthButtonMarkup = (string) ($responsiveFullWidthButton['serialized_blocks'] ?? '');
$responsiveFullWidthButtonCss = implode("\n", array_map(static fn (array $asset): string => 'css' === ($asset['kind'] ?? '') ? (string) ($asset['content'] ?? '') : '', $responsiveFullWidthButton['assets'] ?? array()));
$responsiveFullWidthButtonMarker = preg_match('/\bblocks-engine-control-[^\s"]+/', $responsiveFullWidthButtonMarkup, $matches) ? $matches[0] : '';
$assert(str_contains($responsiveFullWidthButtonCss, '@media(min-width:992px)') && str_contains($responsiveFullWidthButtonCss, 'width:auto'), 'responsive full-width button retains its desktop intrinsic-width author rule');
$assert(str_contains($responsiveFullWidthButtonMarkup, '<img src="assets/materialized-svg/') && str_contains($responsiveFullWidthButtonMarkup, '<span>Sign Up</span>'), 'responsive full-width button retains nested icon and label content');
$assert(str_contains($responsiveFullWidthButtonCss, 'display:flex!important;align-items:center!important;justify-content:center!important;gap:8px!important') && str_contains($responsiveFullWidthButtonCss, '@media(min-width:992px)') && str_contains($responsiveFullWidthButtonCss, 'padding:8px 16px!important') && ! str_contains($responsiveFullWidthButtonCss, 'padding-top:12px!important'), 'responsive icon button keeps its authored flex row and desktop padding instead of a generated mobile padding override');
$assert('' !== $responsiveFullWidthButtonMarker && ! str_contains($responsiveFullWidthButtonCss, ':where(.' . $responsiveFullWidthButtonMarker . '.wp-block-buttons){display:block!important;gap:0!important;width:100%!important}'), 'responsive full-width button does not emit an unconditional generated full-width wrapper bridge');
$assert(str_contains($responsiveFullWidthButtonMarkup, 'wp-block-buttons blocks-engine-control-') && str_contains($responsiveFullWidthButtonMarkup, 'wp-block-button blocks-engine-control-') && 'pass' === ($responsiveFullWidthButton['source_reports']['wp_block_validity']['status'] ?? ''), 'responsive full-width button keeps the canonical wrapper save shape and validity');

$contextualSurfaceButton = ( new HtmlTransformer() )->transform(
'<style>.cta{display:inline-block;border:1px solid #000}.cta .cta-inner{display:inline-block;min-width:170px;padding:22px 26px;border-radius:0;background-color:#00ff8e;color:#000;font-size:16px;line-height:1;font-weight:700}.highlight .cta-inner{background:#fff;color:#000}</style><div style="text-align:center"><a class="cta highlight" href="/learn"><span class="cta-inner">Learn more</span></a></div>'
)->toArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
{ "path": "assets.2.content", "assert": "contains", "value": ".wp-block-buttons{width:max-content;max-width:100%}" },
{ "path": "assets.2.content", "assert": "contains", "value": ".wp-block-button{width:max-content;max-width:100%}" },
{ "path": "assets.2.content", "assert": "contains", "value": "box-sizing:border-box;width:max-content;max-width:100%" },
{ "path": "assets.1.content", "assert": "contains", "value": "padding:12px 24px;border-radius:50px" },
{ "path": "assets.1.content", "assert": "contains", "value": "padding:12px 24px!important;border-radius:50px" },
{ "path": "assets.1.content", "assert": "contains", "value": "@media(min-width:601px){.mobile-variant{display:none}.desktop-variant{display:grid}}" },
{ "path": "fallbacks", "assert": "count", "count": 0 }
]
Expand Down
2 changes: 1 addition & 1 deletion php-transformer/tests/unit/button-signal-classifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
$streamRow = ( new HtmlTransformer() )->transform('<style>.stream-btn{display:inline-flex;padding:10px 16px;background:#135e96;color:#fff;border-radius:4px}</style><div><a class="stream-btn" href="/listen">Listen live</a><a class="stream-btn" href="/schedule">View schedule</a></div>', array())->toArray();
$streamRowCss = implode("\n", array_map(static fn (array $asset): string => (string) ($asset['content'] ?? ''), $streamRow['assets'] ?? array()));
$assert('core/buttons' === ($streamRow['blocks'][0]['blockName'] ?? '') && 2 === count($streamRow['blocks'][0]['innerBlocks'] ?? array()), '22: direct anchor CTA rows group explicit stylesheet surfaces as buttons', json_encode($streamRow['blocks'] ?? array()));
$assert(2 === substr_count($streamRowCss, '> :where(.wp-block-button__link)') && str_contains($streamRowCss, '{display:inline-flex;padding:10px 16px;background:#135e96'), '23: direct anchor CTA stylesheet selectors remain on both rendered button links', $streamRowCss);
$assert(2 === substr_count($streamRowCss, '> :where(.wp-block-button__link)') && str_contains($streamRowCss, '{display:inline-flex!important;padding:10px 16px!important;background:#135e96'), '23: direct anchor CTA stylesheet selectors remain on both rendered button links', $streamRowCss);

$buttonResult = ( new HtmlTransformer() )->transform('<button style="padding:12px 18px;background:#135e96;color:#fff">Buy tickets</button>', array())->toArray();
$nativeButton = $buttonResult['blocks'][0]['innerBlocks'][0] ?? array();
Expand Down
Loading
Loading