diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 59a3bbc3eb2ef6..3aa07158df8534 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1145,9 +1145,23 @@ protected function process_blocks_custom_css( $css, $selector ) { // Split CSS nested rules. $parts = explode( '&', $css ); foreach ( $parts as $part ) { - $processed_css .= ( ! str_contains( $part, '{' ) ) - ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level. - : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character. + $is_root_css = ( ! str_contains( $part, '{' ) ); + if ( $is_root_css ) { + // If the part doesn't contain braces, it applies to the root level. + $processed_css .= trim( $selector ) . '{' . trim( $part ) . '}'; + } else { + // If the part contains braces, it's a nested CSS rule. + $part = explode( '{', str_replace( '}', '', $part ) ); + if ( count( $part ) !== 2 ) { + continue; + } + $nested_selector = $part[0]; + $css_value = $part[1]; + $part_selector = str_starts_with( $nested_selector, ' ' ) + ? static::scope_selector( $selector, $nested_selector ) + : static::append_to_selector( $selector, $nested_selector ); + $processed_css .= $part_selector . '{' . trim( $css_value ) . '}'; + } } return $processed_css; } diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 1aead846e95cdb..b05381a8325b06 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -14,6 +14,7 @@ import { toCustomProperties, toStyles, getStylesDeclarations, + processCSSNesting, } from '../use-global-styles-output'; import { ROOT_BLOCK_SELECTOR } from '../utils'; @@ -967,4 +968,42 @@ describe( 'global styles renderer', () => { ] ); } ); } ); + + describe( 'processCSSNesting', () => { + it( 'should return processed CSS without any nested selectors', () => { + expect( + processCSSNesting( 'color: red; margin: auto;', '.foo' ) + ).toEqual( '.foo{color: red; margin: auto;}' ); + } ); + it( 'should return processed CSS with nested selectors', () => { + expect( + processCSSNesting( + 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;}', + '.foo' + ) + ).toEqual( + '.foo{color: red; margin: auto;}.foo.one{color: blue;}.foo .two{color: green;}' + ); + } ); + 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;}', + '.foo' + ) + ).toEqual( + '.foo{color: red; margin: auto;}.foo::before{color: blue;}.foo ::before{color: green;}.foo.one::before{color: yellow;}.foo .two::before{color: purple;}' + ); + } ); + 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;}', + '.foo, .bar' + ) + ).toEqual( + '.foo, .bar{color: red; margin: auto;}.foo.one, .bar.one{color: blue;}.foo .two, .bar .two{color: green;}.foo::before, .bar::before{color: yellow;}.foo ::before, .bar ::before{color: purple;}.foo.three::before, .bar.three::before{color: orange;}.foo .four::before, .bar .four::before{color: skyblue;}' + ); + } ); + } ); } ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index e32df90695b35b..7e99eca355b52e 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -15,7 +15,12 @@ import { getCSSRules } from '@wordpress/style-engine'; /** * Internal dependencies */ -import { PRESET_METADATA, ROOT_BLOCK_SELECTOR, scopeSelector } from './utils'; +import { + PRESET_METADATA, + ROOT_BLOCK_SELECTOR, + scopeSelector, + appendToSelector, +} from './utils'; import { getBlockCSSSelector } from './get-block-css-selector'; import { getTypographyFontSizeValue, @@ -1124,18 +1129,33 @@ function updateConfigWithSeparator( config ) { return config; } -const processCSSNesting = ( css, blockSelector ) => { +export function processCSSNesting( css, blockSelector ) { let processedCSS = ''; // Split CSS nested rules. const parts = css.split( '&' ); parts.forEach( ( part ) => { - processedCSS += ! part.includes( '{' ) - ? blockSelector + '{' + part + '}' // If the part doesn't contain braces, it applies to the root level. - : blockSelector + part; // Prepend the selector, which effectively replaces the "&" character. + const isRootCss = ! part.includes( '{' ); + if ( isRootCss ) { + // If the part doesn't contain braces, it applies to the root level. + processedCSS += `${ blockSelector }{${ part.trim() }}`; + } else { + // If the part contains braces, it's a nested CSS rule. + const splittedPart = part.replace( '}', '' ).split( '{' ); + if ( splittedPart.length !== 2 ) { + return; + } + + const [ nestedSelector, cssValue ] = splittedPart; + const combinedSelector = nestedSelector.startsWith( ' ' ) + ? scopeSelector( blockSelector, nestedSelector ) + : appendToSelector( blockSelector, nestedSelector ); + + processedCSS += `${ combinedSelector }{${ cssValue.trim() }}`; + } } ); return processedCSS; -}; +} /** * Returns the global styles output using a global styles configuration. diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index d4f2d959a33659..f4adb7a7903122 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -393,6 +393,27 @@ export function scopeSelector( scope, selector ) { return selectorsScoped.join( ', ' ); } +/** + * Appends a sub-selector to an existing one. + * + * Given the compounded `selector` "h1, h2, h3" + * and the `toAppend` selector ".some-class" the result will be + * "h1.some-class, h2.some-class, h3.some-class". + * + * @param {string} selector Original selector. + * @param {string} toAppend Selector to append. + * + * @return {string} The new selector. + */ +export function appendToSelector( selector, toAppend ) { + if ( ! selector.includes( ',' ) ) { + return selector + toAppend; + } + const selectors = selector.split( ',' ); + const newSelectors = selectors.map( ( sel ) => sel + toAppend ); + return newSelectors.join( ',' ); +} + /** * Compares global style variations according to their styles and settings properties. * diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 39f308a5791f60..0e212983d9080f 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2007,29 +2007,37 @@ public function test_process_blocks_custom_css( $input, $expected ) { */ public function data_process_blocks_custom_css() { return array( - // Simple CSS without any child selectors. - 'no child selectors' => array( + // Simple CSS without any nested selectors. + 'no nested selectors' => array( 'input' => array( 'selector' => '.foo', 'css' => 'color: red; margin: auto;', ), 'expected' => '.foo{color: red; margin: auto;}', ), - // CSS with child selectors. - 'with children' => array( + // CSS with nested selectors. + 'with nested selector' => array( 'input' => array( 'selector' => '.foo', - 'css' => 'color: red; margin: auto; & .bar{color: blue;}', + 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;}', ), - 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}', + 'expected' => '.foo{color: red; margin: auto;}.foo.one{color: blue;}.foo .two{color: green;}', ), - // CSS with child selectors and pseudo elements. - 'with children and pseudo elements' => array( + // CSS with pseudo elements. + 'with pseudo elements' => array( 'input' => array( 'selector' => '.foo', - 'css' => 'color: red; margin: auto; & .bar{color: blue;} &::before{color: green;}', + 'css' => 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}', ), - 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}.foo::before{color: green;}', + 'expected' => '.foo{color: red; margin: auto;}.foo::before{color: blue;}.foo ::before{color: green;}.foo.one::before{color: yellow;}.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' => '.foo, .bar{color: red; margin: auto;}.foo.one, .bar.one{color: blue;}.foo .two, .bar .two{color: green;}.foo::before, .bar::before{color: yellow;}.foo ::before, .bar ::before{color: purple;}.foo.three::before, .bar.three::before{color: orange;}.foo .four::before, .bar .four::before{color: skyblue;}', ), ); }