Skip to content

Commit

Permalink
Global Styles: Fix block custom CSS pseudo element selectors (#63980)
Browse files Browse the repository at this point in the history
Unlinked contributors: harlet.

Co-authored-by: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: dballari <dballari@git.wordpress.org>
Co-authored-by: wongjn <wongjn@git.wordpress.org>
  • Loading branch information
6 people authored Jul 31, 2024
1 parent c3afc37 commit 245248e
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 13 deletions.
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

0 comments on commit 245248e

Please sign in to comment.