diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index acff2963802c7..bf741637c318e 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -931,6 +931,27 @@ public function get_settings() { } } + /** + * Processes the CSS, to apply nesting. + * + * @param string $css The CSS to process. + * @param string $selector The selector to nest. + * + * @return string The processed CSS. + */ + public function process_blocks_custom_css( $css, $selector ) { + $processed_css = ''; + + // 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. + } + return $processed_css; + } + /** * Returns the stylesheet that results of processing * the theme.json structure this object represents. @@ -1005,7 +1026,19 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' // Load the custom CSS last so it has the highest specificity. if ( in_array( 'custom-css', $types, true ) ) { + // Add the global styles root CSS. $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); + + // Add the global styles block CSS. + if ( isset( $this->theme_json['styles']['blocks'] ) ) { + foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) { + $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) ); + if ( $custom_block_css ) { + $selector = static::$blocks_metadata[ $name ]['selector']; + $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector ); + } + } + } } return $stylesheet; diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 54645bc1dee6b..39dadf3d020e1 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -4698,4 +4698,55 @@ public function data_custom_css_for_user_caps() { ), ); } + + /** + * @dataProvider data_process_blocks_custom_css + * + * @param array $input An array containing the selector and css to test. + * @param string $expected Expected results. + */ + public function test_process_blocks_custom_css( $input, $expected ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array(), + ) + ); + + $this->assertEquals( $expected, $theme_json->process_blocks_custom_css( $input['css'], $input['selector'] ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_process_blocks_custom_css() { + return array( + // Simple CSS without any child selectors. + 'no child selectors' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto;', + ), + 'expected' => '.foo{color: red; margin: auto;}', + ), + // CSS with child selectors. + 'with children' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; & .bar{color: blue;}', + ), + 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}', + ), + // CSS with child selectors and pseudo elements. + 'with children and pseudo elements' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; & .bar{color: blue;} &::before{color: green;}', + ), + 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}.foo::before{color: green;}', + ), + ); + } }