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 @@ -134,20 +134,27 @@ private function rewriteStyleRule(string $prelude, string $body, AuthorStyleshee
return null !== $mixedButtonProjection ? $mixedButtonProjection : $projectedPrelude . '{' . $body . '}';
}
[ $geometry, $inner ] = $this->splitDirectButtonGeometryDeclarations($body);
$nonButtonGeometryPrelude = $this->withoutButtonPresentationProjectionSelectors($projectedPrelude, $directWrapperPrelude);
$nonButtonGeometryDeclarations = array_filter(array( $geometry, $this->collapsedButtonKeywordWidthDeclarations($body) ));
$nonButtonGeometry = '' === $nonButtonGeometryPrelude || array() === $nonButtonGeometryDeclarations
? ''
: $nonButtonGeometryPrelude . '{' . implode(';', $nonButtonGeometryDeclarations) . '}';
if ( '' === $geometry ) {
return '' === $inner ? '' : $projectedPrelude . '{' . $inner . '}';
return ( '' === $inner ? '' : $projectedPrelude . '{' . $inner . '}' ) . $nonButtonGeometry;
}
return $this->withButtonWrapperInnerFill($directWrapperPrelude, $geometry, '' === $inner ? '' : $projectedPrelude . '{' . $inner . '}');
return $this->withButtonWrapperInnerFill($directWrapperPrelude, $geometry, ( '' === $inner ? '' : $projectedPrelude . '{' . $inner . '}' ) . $nonButtonGeometry);
}

[ $layout, $control ] = $this->splitButtonPresentationDeclarations($body);
$nonButtonLayoutPrelude = $this->withoutButtonPresentationProjectionSelectors($projectedPrelude, $wrapperPrelude);
$nonButtonLayout = '' === $nonButtonLayoutPrelude ? '' : $nonButtonLayoutPrelude . '{' . $layout . '}';
if ( '' === $layout ) {
return '' === $control ? '' : $projectedPrelude . '{' . $control . '}';
}
if ( '' === $control ) {
return $this->withButtonWrapperInnerFill($wrapperPrelude, $layout);
return $this->withButtonWrapperInnerFill($wrapperPrelude, $layout, $nonButtonLayout);
}
return $this->withButtonWrapperInnerFill($wrapperPrelude, $layout, $projectedPrelude . '{' . $control . '}');
return $this->withButtonWrapperInnerFill($wrapperPrelude, $layout, $projectedPrelude . '{' . $control . '}' . $nonButtonLayout);
}

/**
Expand Down Expand Up @@ -344,7 +351,7 @@ private function buttonPresentationWrapperPrelude(string $prelude, AuthorStylesh
? $context->selectorProjections->controlMarker($path)
: '';
if ( '' === $marker ) {
continue 2;
continue;
}
$markers[] = $marker;
}
Expand Down Expand Up @@ -376,7 +383,7 @@ private function directButtonGeometryWrapperPrelude(string $prelude, AuthorStyle
$path = $element->getNodePath() ?? '';
$marker = $context->selectorProjections->controlMarker($path);
if ( '' === $marker || $context->selectorProjections->isButtonPresentationPath($path) ) {
continue 2;
continue;
}
$rewritten[] = $this->projectControlSelector($selector, $parsed, $marker, $context, true);
}
Expand Down Expand Up @@ -449,15 +456,60 @@ private function splitButtonPresentationDeclarations(string $body): array
return array( implode(';', $layout), implode(';', $control) );
}

/**
* Geometry moved to a presentation wrapper must remain on other source
* elements matched by the same shared selector.
*/
private function withoutButtonPresentationProjectionSelectors(string $projectedPrelude, string $wrapperPrelude): string
{
$selectors = CssStylesheetTransformer::splitSelectorList($projectedPrelude);
if ( null === $selectors || '' === $wrapperPrelude ) {
return $projectedPrelude;
}
preg_match_all('/:where\(\.([^)]*)\)/', $wrapperPrelude, $matches);
$markers = array_unique($matches[1] ?? array());
if ( array() === $markers ) {
return $projectedPrelude;
}
return implode(',', array_filter($selectors, static function (string $selector) use ($markers): bool {
foreach ( $markers as $marker ) {
$markerSelector = ':where(.' . $marker . ')';
if ( str_contains($selector, $markerSelector) && ! str_contains($selector, ':not(' . $markerSelector . ')') ) {
return false;
}
}
return true;
}));
}

private function withButtonWrapperInnerFill(string $wrapperPrelude, string $layoutCss, string $rest = ''): string
{
$css = $wrapperPrelude . '{' . $layoutCss . '}';
if ( CssValueInspector::hasDefiniteWidth($layoutCss) ) {
$hasDefiniteWidth = CssValueInspector::hasDefiniteWidth($layoutCss);
$hasDefiniteHeight = CssValueInspector::hasDefiniteHeight($layoutCss);
$hasAutoHeight = CssValueInspector::hasAutoHeight($layoutCss);
$hasMinimumHeight = CssValueInspector::hasAuthoredMinimumHeight($layoutCss);
if ( $hasDefiniteWidth || $hasDefiniteHeight || $hasAutoHeight || $hasMinimumHeight ) {
$selectors = CssStylesheetTransformer::splitSelectorList($wrapperPrelude) ?? array( $wrapperPrelude );
$button = implode(',', array_map(static fn (string $selector): string => rtrim($selector) . '> :where(.wp-block-button)', $selectors));
$link = implode(',', array_map(static fn (string $selector): string => rtrim($selector) . '> :where(.wp-block-button)> :where(.wp-block-button__link)', $selectors));
$css .= $button . '{width:100%!important}'
. $link . '{width:100%!important;max-width:100%!important}';
if ( $hasDefiniteWidth ) {
$css .= $button . '{width:100%!important}'
. $link . '{width:100%!important;max-width:100%!important}';
}
if ( $hasDefiniteHeight ) {
$css .= $button . '{height:100%!important}'
. $link . '{height:100%!important}';
} elseif ( $hasAutoHeight ) {
$css .= $button . '{height:auto!important}'
. $link . '{height:auto!important}';
}
if ( $hasMinimumHeight ) {
// Percentage heights cannot resolve through an auto-height wrapper.
// Inherit the wrapper's authored computed minimum on both carriers.
$css .= $button . '{min-height:inherit!important}'
. $link . '{min-height:inherit!important}';
}
}
return $css . $rest;
}
Expand All @@ -481,6 +533,20 @@ private function isCollapsedButtonKeywordWidth(string $property, string $value):
|| (str_starts_with($property, '--') && str_contains($property, 'width'));
}

private function collapsedButtonKeywordWidthDeclarations(string $body): string
{
$collapsed = array();
foreach ( CssValueSplitter::splitTopLevel($body, array( ';' )) as $declaration ) {
$colon = strpos($declaration, ':');
$name = strtolower(trim(false === $colon ? $declaration : substr($declaration, 0, $colon)));
$value = false === $colon ? '' : trim(substr($declaration, $colon + 1));
if ( false !== $colon && $this->isCollapsedButtonKeywordWidth($name, $value) ) {
$collapsed[] = $declaration;
}
}
return implode(';', $collapsed);
}

private function isButtonWrapperLayoutProperty(string $property): bool
{
return in_array($property, array(
Expand Down Expand Up @@ -963,7 +1029,7 @@ private function rewriteSourceTagTypes(string $selector, array $parsed, AuthorSt
private function projectControlSelector(string $selector, array $parsed, string $marker, AuthorStylesheetProjectionContext $context, bool $wrapper = false): string
{
$suffix = null === $parsed['pseudo_state_suffix_span'] ? '' : substr($selector, $parsed['pseudo_state_suffix_span']['start']);
return ':where(.' . $marker . ')' . ($wrapper ? ':where(.wp-block-buttons)' : $this->selectorSpecificityShims($parsed, $context) . '> :where(.wp-block-button__link)') . $suffix;
return ':where(.' . $marker . ')' . $this->selectorSpecificityShims($parsed, $context) . ($wrapper ? ':where(.wp-block-buttons)' : '> :where(.wp-block-button__link)') . $suffix;
}

/** @param array<string, mixed> $parsed */
Expand Down
46 changes: 46 additions & 0 deletions php-transformer/src/HtmlToBlocks/Style/CssValueInspector.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,50 @@ public static function hasDefiniteWidth(string $css): bool
}
return false;
}

public static function hasDefiniteHeight(string $css): bool
{
return self::hasAuthoredLengthProperty($css, 'height');
}

public static function hasAuthoredMinimumHeight(string $css): bool
{
return self::hasAuthoredLengthProperty($css, 'min-height');
}

private static function hasAuthoredLengthProperty(string $css, string $property): bool
{
foreach ( CssValueSplitter::splitTopLevel($css, array( ';' )) as $declaration ) {
$colon = strpos($declaration, ':');
if ( false === $colon || $property !== strtolower(trim(substr($declaration, 0, $colon))) ) {
continue;
}
$value = strtolower(self::withoutImportant(substr($declaration, $colon + 1)));
if ( '' === $value || in_array($value, array( 'auto', 'inherit', 'initial', 'unset', 'none', 'min-content', 'max-content', 'fit-content', 'content' ), true) ) {
continue;
}
// A bare custom property may resolve to a keyword such as auto. CSS math
// functions, including ones containing vars, remain authored lengths.
if ( str_contains($value, 'var(')
&& 1 !== preg_match('/^(?:calc|min|max|clamp|round|mod|rem|sin|cos|tan|asin|acos|atan|atan2|pow|sqrt|hypot|log|exp|abs|sign)\(/', $value) ) {
continue;
}
return true;
}
return false;
}

public static function hasAutoHeight(string $css): bool
{
foreach ( CssValueSplitter::splitTopLevel($css, array( ';' )) as $declaration ) {
$colon = strpos($declaration, ':');
if ( false !== $colon
&& 'height' === strtolower(trim(substr($declaration, 0, $colon)))
&& 'auto' === strtolower(self::withoutImportant(substr($declaration, $colon + 1)))
) {
return true;
}
}
return false;
}
}
73 changes: 72 additions & 1 deletion php-transformer/tests/unit/button-wrapper-inner-fill.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
declare(strict_types=1);

/**
* A definite width on `.wp-block-buttons` must fill the inner link (issue #1303).
* Definite source-owned core/buttons geometry must fill the native inner carriers.
*/

require dirname(__DIR__, 2) . '/vendor/autoload.php';
Expand Down Expand Up @@ -47,6 +47,77 @@
$css
);

$height = ( new HtmlTransformer() )->transform(
'<style>#source-button{height:45.8594px;padding:12px 24px;font-size:16px;background:#173b64;color:#fff}</style>'
. '<main><a id="source-button" href="/quote">GET A QUOTE</a></main>'
)->toArray();
$heightCss = '';
foreach ( $height['assets'] ?? array() as $asset ) {
if ( is_array($asset) && 'css' === ( $asset['kind'] ?? '' ) ) {
$heightCss .= (string) ( $asset['content'] ?? '' );
}
}

$assert(
(bool) preg_match('/wp-block-buttons[^}]*\{[^}]*height:45\.8594px/', $heightCss),
'4: source-authored height stays on the outer core/buttons carrier',
$heightCss
);
$assert(
(bool) preg_match('/wp-block-buttons\)> :where\(\.wp-block-button\)\{height:100%!important\}[^\n]*wp-block-button__link\)\{height:100%!important\}/', $heightCss),
'5: a definite outer height fills the nested core/button and link from the authored carrier rule',
$heightCss
);

$autoHeight = ( new HtmlTransformer() )->transform(
'<style>#auto-button{height:auto;padding:12px 24px;font-size:16px;background:#173b64;color:#fff}</style>'
. '<main><a id="auto-button" href="/quote">GET A QUOTE</a></main>'
)->toArray();
$autoHeightCss = implode('', array_map(static fn (array $asset): string => 'css' === ( $asset['kind'] ?? '' ) ? (string) ( $asset['content'] ?? '' ) : '', $autoHeight['assets'] ?? array()));
$assert(
! str_contains($autoHeightCss, 'height:100%!important'),
'6: auto-height does not opt into inner carrier fill',
$autoHeightCss
);

$responsiveHeight = ( new HtmlTransformer() )->transform(
'<style>#responsive-button{height:45.8594px;padding:12px 24px;background:#173b64;color:#fff}@media(max-width:600px){#responsive-button{height:auto}}</style>'
. '<main><a id="responsive-button" href="/quote">GET A QUOTE</a></main>'
)->toArray();
$responsiveHeightCss = implode('', array_map(static fn (array $asset): string => 'css' === ( $asset['kind'] ?? '' ) ? (string) ( $asset['content'] ?? '' ) : '', $responsiveHeight['assets'] ?? array()));
$assert(
(bool) preg_match('/@media\(max-width:600px\)\{[^}]*height:auto[^}]*\}[^@]*height:auto!important/', $responsiveHeightCss),
'7: responsive auto-height explicitly clears the nested carrier fill in the same condition',
$responsiveHeightCss
);

$mathMinimumHeight = ( new HtmlTransformer() )->transform(
'<style>:root{--scaling-factor:400px;--scrollbar-width:8px}.cta{height:auto;min-height:max(.5px,.1175977*(var(--scaling-factor) - var(--scrollbar-width)));padding:12px 24px;background:#173b64;color:#fff}@media(max-width:600px){.cta{min-height:0}}</style>'
. '<main><a class="cta" href="/quote">GET A QUOTE</a></main>'
)->toArray();
$mathMinimumHeightCss = implode('', array_map(static fn (array $asset): string => 'css' === ( $asset['kind'] ?? '' ) ? (string) ( $asset['content'] ?? '' ) : '', $mathMinimumHeight['assets'] ?? array()));
$assert(
str_contains($mathMinimumHeightCss, 'min-height:max(.5px,.1175977*(var(--scaling-factor) - var(--scrollbar-width)))')
&& 4 === substr_count($mathMinimumHeightCss, 'min-height:inherit!important'),
'8: variable-backed math minimum height stays on the outer carrier and is inherited by both native carriers',
$mathMinimumHeightCss
);
$assert(
(bool) preg_match('/@media\(max-width:600px\)\{[^}]*min-height:0[^}]*\}[^@]*min-height:inherit!important/', $mathMinimumHeightCss),
'9: responsive minimum-height reset reaches both native carriers in the same condition',
$mathMinimumHeightCss
);

$pureVariableHeight = ( new HtmlTransformer() )->transform(
'<style>.cta{height:var(--possibly-auto);padding:12px 24px;background:#173b64;color:#fff}</style><main><a class="cta" href="/quote">GET A QUOTE</a></main>'
)->toArray();
$pureVariableHeightCss = implode('', array_map(static fn (array $asset): string => 'css' === ( $asset['kind'] ?? '' ) ? (string) ( $asset['content'] ?? '' ) : '', $pureVariableHeight['assets'] ?? array()));
$assert(
! str_contains($pureVariableHeightCss, 'height:100%!important'),
'10: a bare custom-property height remains conservative because it can resolve to auto',
$pureVariableHeightCss
);

if ( $failures > 0 ) {
fwrite(STDERR, PHP_EOL . "button wrapper inner fill tests: {$passes} passed, {$failures} FAILED" . PHP_EOL);
exit(1);
Expand Down
2 changes: 1 addition & 1 deletion php-transformer/tests/unit/engine-support-css-asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
$assert(1 === count($authorAssets), 'G2: transform emits exactly one author-css asset');
$normalizedAuthorCss = preg_replace('/\s+/', '', (string) ($authorAssets[0]['content'] ?? '')) ?? '';
$assert(
'@layercontract;.contract-author-only{color:#123456}.desktop-nava{color:#fff}:where(.blocks-engine-control-6494fb2a0d77-3):where(.wp-block-buttons){width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):where(.wp-block-buttons)>:where(.wp-block-button){width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):where(.wp-block-buttons)>:where(.wp-block-button)>:where(.wp-block-button__link){width:100%!important;max-width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):not(.blocks-engine-specificity-class-6494fb2a0d77-1)>:where(.wp-block-button__link){display:inline-flex!important;padding:1rem!important;background:#123456}@media(max-width:700px){.desktop-nav{display:none}.mobile-nav{background:rgba(0,0,0,.9)}}' === $normalizedAuthorCss,
'@layercontract;.contract-author-only{color:#123456}.desktop-nava{color:#fff}:where(.blocks-engine-control-6494fb2a0d77-3):not(.blocks-engine-specificity-class-6494fb2a0d77-1):where(.wp-block-buttons){width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):not(.blocks-engine-specificity-class-6494fb2a0d77-1):where(.wp-block-buttons)>:where(.wp-block-button){width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):not(.blocks-engine-specificity-class-6494fb2a0d77-1):where(.wp-block-buttons)>:where(.wp-block-button)>:where(.wp-block-button__link){width:100%!important;max-width:100%!important}:where(.blocks-engine-control-6494fb2a0d77-3):not(.blocks-engine-specificity-class-6494fb2a0d77-1)>:where(.wp-block-button__link){display:inline-flex!important;padding:1rem!important;background:#123456}@media(max-width:700px){.desktop-nav{display:none}.mobile-nav{background:rgba(0,0,0,.9)}}' === $normalizedAuthorCss,
'G2: author-css contains only its leading at-rule preamble and rewritten author stylesheet'
);
$assert('author' === ($authorAssets[0]['stylesheet_placement'] ?? ''), 'G4: author-css record declares author placement');
Expand Down
2 changes: 1 addition & 1 deletion php-transformer/tools/visual-parity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"install:browsers": "playwright install chromium",
"test": "node tests/smoke.mjs && node tests/blockquote-margin-reset.mjs && node tests/exact-fit-inline-flow.mjs && node tests/layout-shell-editor-geometry.mjs && node tests/custom-video-host-editor-geometry.mjs && node tests/issue-1493-reduced-public-fixture.mjs && node tests/responsive-document-variants.mjs && node tests/image-custom-host-promotion.mjs"
"test": "node tests/smoke.mjs && node tests/blockquote-margin-reset.mjs && node tests/exact-fit-inline-flow.mjs && node tests/layout-shell-editor-geometry.mjs && node tests/custom-video-host-editor-geometry.mjs && node tests/issue-1493-reduced-public-fixture.mjs && node tests/responsive-document-variants.mjs && node tests/image-custom-host-promotion.mjs && node tests/button-height-ownership.mjs"
},
"devDependencies": {
"playwright": "^1.56.0"
Expand Down
Loading
Loading