diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 2e39b3c3530d2..1f03a675c51da 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -665,9 +665,24 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks = array(); $schema_settings_blocks = array(); foreach ( $valid_block_names as $block ) { - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + // Build the schema for each block style variation. + $style_variation_names = array(); + if ( + ! empty( $input['styles']['blocks'][ $block ]['variations'] ) && + is_array( $input['styles']['blocks'][ $block ]['variations'] ) + ) { + $style_variation_names = array_keys( $input['styles']['blocks'][ $block ]['variations'] ); + } + + $schema_styles_variations = array(); + if ( ! empty( $style_variation_names ) ) { + $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level ); + } + + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } $schema['styles'] = static::VALID_STYLES; @@ -814,6 +829,15 @@ protected static function get_blocks_metadata() { } static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); } + // If the block has style variations, append their selectors to the block metadata. + if ( ! empty( $block_type->styles ) ) { + $style_selectors = array(); + foreach ( $block_type->styles as $style ) { + // The style variation classname is duplicated in the selector to ensure that it overrides core block styles. + $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + } + static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; + } } return static::$blocks_metadata; @@ -2039,12 +2063,23 @@ private static function get_block_nodes( $theme_json ) { $feature_selectors = $selectors[ $name ]['features']; } + $variation_selectors = array(); + if ( isset( $node['variations'] ) ) { + foreach ( $node['variations'] as $variation => $node ) { + $variation_selectors[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), + 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], + ); + } + } + $nodes[] = array( - 'name' => $name, - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'duotone' => $duotone_selector, - 'features' => $feature_selectors, + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name ), + 'selector' => $selector, + 'duotone' => $duotone_selector, + 'features' => $feature_selectors, + 'variations' => $variation_selectors, ); if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { @@ -2124,6 +2159,54 @@ public function get_styles_for_block( $block_metadata ) { } } + // If there are style variations, generate the declarations for them, including any feature selectors the block may have. + $style_variation_declarations = array(); + if ( ! empty( $block_metadata['variations'] ) ) { + foreach ( $block_metadata['variations'] as $style_variation ) { + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); + $style_variation_selector = $style_variation['selector']; + + // If the block has feature selectors, generate the declarations for them within the current style variation. + if ( ! empty( $block_metadata['features'] ) ) { + $clean_style_variation_selector = trim( $style_variation_selector ); + foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { + if ( empty( $style_variation_node[ $feature_name ] ) ) { + continue; + } + // Prepend the variation selector to the feature selector. + $split_feature_selectors = explode( ',', $feature_selector ); + $feature_selectors = array_map( + static function( $split_feature_selector ) use ( $clean_style_variation_selector ) { + return $clean_style_variation_selector . trim( $split_feature_selector ); + }, + $split_feature_selectors + ); + $combined_feature_selectors = implode( ',', $feature_selectors ); + + // Compute declarations for the feature. + $new_feature_declarations = static::compute_style_properties( array( $feature_name => $style_variation_node[ $feature_name ] ), $settings, null, $this->theme_json ); + + /* + * Merge new declarations with any that already exist for + * the feature selector. This may occur when multiple block + * support features use the same custom selector. + */ + if ( isset( $style_variation_declarations[ $combined_feature_selectors ] ) ) { + $style_variation_declarations[ $combined_feature_selectors ] = array_merge( $style_variation_declarations[ $combined_feature_selectors ], $new_feature_declarations ); + } else { + $style_variation_declarations[ $combined_feature_selectors ] = $new_feature_declarations; + } + /* + * Remove the feature from the variation's node now the + * styles will be included under the feature level selector. + */ + unset( $style_variation_node[ $feature_name ] ); + } + } + // Compute declarations for remaining styles not covered by feature level selectors. + $style_variation_declarations[ $style_variation_selector ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); + } + } /* * Get a reference to element name from path. * $block_metadata['path'] = array( 'styles','elements','link' ); @@ -2214,6 +2297,11 @@ function( $pseudo_selector ) use ( $selector ) { $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); } + // 6. Generate and append the style variation rulesets. + foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) { + $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations ); + } + return $block_rules; } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 3f174b5d05cbe..a030506ec4b0d 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -3595,6 +3595,306 @@ public function test_get_styles_for_block_with_content_width() { $this->assertSame( $expected, $root_rules . $style_rules ); } + /** + * @ticket 57583 + * + * @dataProvider data_sanitize_for_block_with_style_variations + * + * @param array $theme_json_variations Theme.json variations to test. + * @param array $expected_sanitized Expected results after sanitizing. + */ + public function test_sanitize_for_block_with_style_variations( $theme_json_variations, $expected_sanitized ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate structure is sanitized. + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertIsArray( $sanitized_theme_json, 'Sanitized theme.json is not an array data type' ); + $this->assertArrayHasKey( 'styles', $sanitized_theme_json, 'Sanitized theme.json does not have an "styles" key' ); + $this->assertSameSetsWithIndex( $expected_sanitized, $sanitized_theme_json['styles'], 'Sanitized theme.json styles does not match' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_for_block_with_style_variations() { + return array( + '1 variation with 1 invalid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ), + '1 variation with 2 invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ), + '2 variations with 1 invalid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + ), + 'basic' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + 'foo' => 'bar', + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + 'basic' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + ), + ), + ), + ), + ), + ), + ); + } + + /** + * @ticket 57583 + * + * @dataProvider data_sanitize_with_invalid_style_variation + * + * @param array $theme_json_variations The theme.json variations to test. + */ + public function test_sanitize_with_invalid_style_variation( $theme_json_variations ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate structure is sanitized. + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertIsArray( $sanitized_theme_json, 'Sanitized theme.json is not an array data type' ); + $this->assertArrayNotHasKey( 'styles', $sanitized_theme_json, 'Sanitized theme.json should not have a "styles" key' ); + + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_with_invalid_style_variation() { + return array( + 'empty string variation' => array( + array( + 'variations' => '', + ), + ), + 'boolean variation' => array( + array( + 'variations' => false, + ), + ), + ); + } + + /** + * @ticket 57583 + * + * @dataProvider data_get_styles_for_block_with_style_variations + * + * @param array $theme_json_variations Theme.json variations to test. + * @param string $metadata_variations Style variations to test. + * @param string $expected Expected results for styling. + */ + public function test_get_styles_for_block_with_style_variations( $theme_json_variations, $metadata_variations, $expected ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate styles are generated properly. + $metadata = array( + 'path' => array( 'styles', 'blocks', 'core/quote' ), + 'selector' => '.wp-block-quote', + 'variations' => $metadata_variations, + ); + $actual_styles = $theme_json->get_styles_for_block( $metadata ); + $this->assertSame( $expected, $actual_styles ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_styles_for_block_with_style_variations() { + $plain = array( + 'metadata' => array( + 'path' => array( 'styles', 'blocks', 'core/quote', 'variations', 'plain' ), + 'selector' => '.is-style-plain.is-style-plain.wp-block-quote', + ), + 'styles' => '.is-style-plain.is-style-plain.wp-block-quote{background-color: hotpink;}', + ); + $basic = array( + 'metadata' => array( + 'path' => array( 'styles', 'blocks', 'core/quote', 'variations', 'basic' ), + 'selector' => '.is-style-basic.is-style-basic.wp-block-quote', + ), + 'styles' => '.is-style-basic.is-style-basic.wp-block-quote{background-color: #ffffff;color: #000000;}', + ); + + return array( + '1 variation with 1 invalid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'] ), + 'expected' => $plain['styles'], + ), + '1 variation with 2 invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'] ), + 'expected' => $plain['styles'], + ), + '2 variations with 1 invalid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + ), + 'basic' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + 'foo' => 'bar', + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'], $basic['metadata'] ), + 'expected_styles' => $plain['styles'] . $basic['styles'], + ), + '2 variations with multiple invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + 'basic' => array( + 'foo' => 'foo', + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + 'bar' => 'bar', + 'baz' => 'baz', + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'], $basic['metadata'] ), + 'expected_styles' => $plain['styles'] . $basic['styles'], + ), + ); + } + /** * @ticket 56611 */