Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global Styles: Fix block custom CSS pseudo element selectors #63980

Merged
merged 6 commits into from
Jul 31, 2024
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
3 changes: 3 additions & 0 deletions backport-changelog/6.6/7097.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/7097

* https://github.com/WordPress/gutenberg/pull/63980
26 changes: 23 additions & 3 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -1443,9 +1443,16 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets'
protected function process_blocks_custom_css( $css, $selector ) {
$processed_css = '';

if ( empty( $css ) ) {
return $processed_css;
}

// Split CSS nested rules.
$parts = explode( '&', $css );
foreach ( $parts as $part ) {
if ( empty( $part ) ) {
continue;
}
$is_root_css = ( ! str_contains( $part, '{' ) );
if ( $is_root_css ) {
// If the part doesn't contain braces, it applies to the root level.
Expand All @@ -1458,11 +1465,24 @@ protected function process_blocks_custom_css( $css, $selector ) {
}
$nested_selector = $part[0];
$css_value = $part[1];
$part_selector = str_starts_with( $nested_selector, ' ' )

/*
* Handle pseudo elements such as ::before, ::after etc. Regex will also
* capture any leading combinator such as >, +, or ~, as well as spaces.
* This allows pseudo elements as descendants e.g. `.parent ::before`.
*/
$matches = array();
$has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
$pseudo_part = $has_pseudo_element ? $matches[1] : '';
$nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;

// Finalize selector and re-append pseudo element if required.
$part_selector = str_starts_with( $nested_selector, ' ' )
? static::scope_selector( $selector, $nested_selector )
: static::append_to_selector( $selector, $nested_selector );
$final_selector = ":root :where($part_selector)";
$processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
$final_selector = ":root :where($part_selector)$pseudo_part";

$processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
}
}
return $processed_css;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1031,11 +1031,19 @@ describe( 'global styles renderer', () => {
} );

describe( 'processCSSNesting', () => {
it( 'should return empty string when supplied css is empty', () => {
expect( processCSSNesting( '', '.foo' ) ).toEqual( '' );
} );
it( 'should return processed CSS without any nested selectors', () => {
expect(
processCSSNesting( 'color: red; margin: auto;', '.foo' )
).toEqual( ':root :where(.foo){color: red; margin: auto;}' );
} );
it( 'should return processed CSS when there are no root selectors', () => {
expect(
processCSSNesting( '&::before{color: red;}', '.foo' )
).toEqual( ':root :where(.foo)::before{color: red;}' );
} );
it( 'should return processed CSS with nested selectors', () => {
expect(
processCSSNesting(
Expand All @@ -1049,21 +1057,21 @@ describe( 'global styles renderer', () => {
it( 'should return processed CSS with pseudo elements', () => {
expect(
processCSSNesting(
'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}',
'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;} & > ::before{color: darkseagreen;}',
'.foo'
)
).toEqual(
':root :where(.foo){color: red; margin: auto;}:root :where(.foo::before){color: blue;}:root :where(.foo ::before){color: green;}:root :where(.foo.one::before){color: yellow;}:root :where(.foo .two::before){color: purple;}'
':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}:root :where(.foo) > ::before{color: darkseagreen;}'
);
} );
it( 'should return processed CSS with multiple root selectors', () => {
expect(
processCSSNesting(
'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}',
'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;} & > ::before{color: darkseagreen;}',
'.foo, .bar'
)
).toEqual(
':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo::before, .bar::before){color: yellow;}:root :where(.foo ::before, .bar ::before){color: purple;}:root :where(.foo.three::before, .bar.three::before){color: orange;}:root :where(.foo .four::before, .bar .four::before){color: skyblue;}'
':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}:root :where(.foo, .bar) > ::before{color: darkseagreen;}'
);
} );
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1327,9 +1327,17 @@ function updateConfigWithSeparator( config ) {
export function processCSSNesting( css, blockSelector ) {
let processedCSS = '';

if ( ! css || css.trim() === '' ) {
return processedCSS;
}

// Split CSS nested rules.
const parts = css.split( '&' );
parts.forEach( ( part ) => {
if ( ! part || part.trim() === '' ) {
return;
}

const isRootCss = ! part.includes( '{' );
if ( isRootCss ) {
// If the part doesn't contain braces, it applies to the root level.
Expand All @@ -1342,11 +1350,32 @@ export function processCSSNesting( css, blockSelector ) {
}

const [ nestedSelector, cssValue ] = splittedPart;
const combinedSelector = nestedSelector.startsWith( ' ' )
? scopeSelector( blockSelector, nestedSelector )
: appendToSelector( blockSelector, nestedSelector );

processedCSS += `:root :where(${ combinedSelector }){${ cssValue.trim() }}`;
// Handle pseudo elements such as ::before, ::after, etc. Regex will also
// capture any leading combinator such as >, +, or ~, as well as spaces.
// This allows pseudo elements as descendants e.g. `.parent ::before`.
const matches = nestedSelector.match( /([>+~\s]*::[a-zA-Z-]+)/ );
const pseudoPart = matches ? matches[ 1 ] : '';
const withoutPseudoElement = matches
? nestedSelector.replace( pseudoPart, '' ).trim()
: nestedSelector.trim();

let combinedSelector;
if ( withoutPseudoElement === '' ) {
// Only contained a pseudo element to use the block selector to form
// the final `:root :where()` selector.
combinedSelector = blockSelector;
} else {
// If the nested selector is a descendant of the block scope it with the
// block selector. Otherwise append it to the block selector.
combinedSelector = nestedSelector.startsWith( ' ' )
? scopeSelector( blockSelector, withoutPseudoElement )
: appendToSelector( blockSelector, withoutPseudoElement );
}

// Build final rule, re-adding any pseudo element outside the `:where()`
// to maintain valid CSS selector.
processedCSS += `:root :where(${ combinedSelector })${ pseudoPart }{${ cssValue.trim() }}`;
}
} );
return processedCSS;
Expand Down
18 changes: 16 additions & 2 deletions phpunit/class-wp-theme-json-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -5004,13 +5004,27 @@ public function test_process_blocks_custom_css( $input, $expected ) {
public function data_process_blocks_custom_css() {
return array(
// Simple CSS without any nested selectors.
'empty css' => array(
'input' => array(
'selector' => '.foo',
'css' => '',
),
'expected' => '',
),
'no nested selectors' => array(
'input' => array(
'selector' => '.foo',
'css' => 'color: red; margin: auto;',
),
'expected' => ':root :where(.foo){color: red; margin: auto;}',
),
'no root styles' => array(
'input' => array(
'selector' => '.foo',
'css' => '&::before{color: red;}',
),
'expected' => ':root :where(.foo)::before{color: red;}',
),
// CSS with nested selectors.
'with nested selector' => array(
'input' => array(
Expand All @@ -5025,15 +5039,15 @@ public function data_process_blocks_custom_css() {
'selector' => '.foo',
'css' => 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}',
),
'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo::before){color: blue;}:root :where(.foo ::before){color: green;}:root :where(.foo.one::before){color: yellow;}:root :where(.foo .two::before){color: purple;}',
'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}',
),
// CSS with multiple root selectors.
'with multiple root selectors' => array(
'input' => array(
'selector' => '.foo, .bar',
'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}',
),
'expected' => ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo::before, .bar::before){color: yellow;}:root :where(.foo ::before, .bar ::before){color: purple;}:root :where(.foo.three::before, .bar.three::before){color: orange;}:root :where(.foo .four::before, .bar .four::before){color: skyblue;}',
'expected' => ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}',
),
);
}
Expand Down
Loading