From f91931a3638d9bdf8132ea240d2b4bdcafd9b06f Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 31 May 2024 05:47:23 +0000 Subject: [PATCH] Block Themes: Add section styling via extended block style variations Provide users with the ability to style entire sections of a page without having to tediously reapply the same sets of styles. This is done by extending block style variations to apply to nested blocks. See https://github.com/WordPress/gutenberg/pull/57908. Fixes #61312. Props aaronrobertshaw, talldanwp, ramonopoly, isabel_brison, andrewserong. git-svn-id: https://develop.svn.wordpress.org/trunk@58264 602fd350-edb4-49c9-b593-d223f7449a82 --- .../block-supports/block-style-variations.php | 424 ++++++++++++++++++ .../class-wp-theme-json-resolver.php | 34 +- src/wp-includes/class-wp-theme-json.php | 104 ++++- src/wp-settings.php | 1 + .../style.css | 8 + .../styles/block-style-variation-a.json | 10 + .../theme.json | 4 + .../styles/block-style-variation-a.json | 10 + .../styles/block-style-variation-b.json | 10 + .../block-supports/block-style-variations.php | 132 ++++++ ...pCreateBlockStyleVariationInstanceName.php | 24 + .../wpGetBlockStyleVariationNameFromClass.php | 65 +++ tests/phpunit/tests/theme/themeDir.php | 1 + .../tests/theme/wpThemeJsonResolver.php | 163 ++++--- 14 files changed, 920 insertions(+), 70 deletions(-) create mode 100644 src/wp-includes/block-supports/block-style-variations.php create mode 100644 tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css create mode 100644 tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json create mode 100644 tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json create mode 100644 tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json create mode 100644 tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json create mode 100644 tests/phpunit/tests/block-supports/block-style-variations.php create mode 100644 tests/phpunit/tests/block-supports/wpCreateBlockStyleVariationInstanceName.php create mode 100644 tests/phpunit/tests/block-supports/wpGetBlockStyleVariationNameFromClass.php diff --git a/src/wp-includes/block-supports/block-style-variations.php b/src/wp-includes/block-supports/block-style-variations.php new file mode 100644 index 0000000000000..c8ba6e75aaa80 --- /dev/null +++ b/src/wp-includes/block-supports/block-style-variations.php @@ -0,0 +1,424 @@ +get_raw_data(); + + // Only the first block style variation with data is supported. + $variation_data = array(); + foreach ( $variations as $variation ) { + $variation_data = $theme_json['styles']['blocks'][ $parsed_block['blockName'] ]['variations'][ $variation ] ?? array(); + + if ( ! empty( $variation_data ) ) { + break; + } + } + + if ( empty( $variation_data ) ) { + return $parsed_block; + } + + $variation_instance = wp_create_block_style_variation_instance_name( $parsed_block, $variation ); + $class_name = "is-style-$variation_instance"; + $updated_class_name = $parsed_block['attrs']['className'] . " $class_name"; + + /* + * Even though block style variations are effectively theme.json partials, + * they can't be processed completely as though they are. + * + * Block styles support custom selectors to direct specific types of styles + * to inner elements. For example, borders on Image block's get applied to + * the inner `img` element rather than the wrapping `figure`. + * + * The following relocates the "root" block style variation styles to + * under an appropriate blocks property to leverage the preexisting style + * generation for simple block style variations. This way they get the + * custom selectors they need. + * + * The inner elements and block styles for the variation itself are + * still included at the top level but scoped by the variation's selector + * when the stylesheet is generated. + */ + $elements_data = $variation_data['elements'] ?? array(); + $blocks_data = $variation_data['blocks'] ?? array(); + unset( $variation_data['elements'] ); + unset( $variation_data['blocks'] ); + + _wp_array_set( + $blocks_data, + array( $parsed_block['blockName'], 'variations', $variation_instance ), + $variation_data + ); + + $config = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => $elements_data, + 'blocks' => $blocks_data, + ), + ); + + // Turn off filter that excludes block nodes. They are needed here for the variation's inner block types. + if ( ! is_admin() ) { + remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); + } + + // Temporarily prevent variation instance from being sanitized while processing theme.json. + $styles_registry = WP_Block_Styles_Registry::get_instance(); + $styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) ); + + $variation_theme_json = new WP_Theme_JSON( $config, 'blocks' ); + $variation_styles = $variation_theme_json->get_stylesheet( + array( 'styles' ), + array( 'custom' ), + array( + 'skip_root_layout_styles' => true, + 'scope' => ".$class_name", + ) + ); + + // Clean up temporary block style now instance styles have been processed. + $styles_registry->unregister( $parsed_block['blockName'], $variation_instance ); + + // Restore filter that excludes block nodes. + if ( ! is_admin() ) { + add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); + } + + if ( empty( $variation_styles ) ) { + return $parsed_block; + } + + wp_register_style( 'block-style-variation-styles', false, array( 'global-styles', 'wp-block-library' ) ); + wp_add_inline_style( 'block-style-variation-styles', $variation_styles ); + + /* + * Add variation instance class name to block's className string so it can + * be enforced in the block markup via render_block filter. + */ + _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); + + return $parsed_block; +} + +/** + * Ensure the variation block support class name generated and added to + * block attributes in the `render_block_data` filter gets applied to the + * block's markup. + * + * @see wp_render_block_style_variation_support_styles + * + * @since 6.6.0 + * @access private + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * + * @return string Filtered block content. + */ +function wp_render_block_style_variation_class_name( $block_content, $block ) { + if ( ! $block_content || empty( $block['attrs']['className'] ) ) { + return $block_content; + } + + /* + * Matches a class prefixed by `is-style`, followed by the + * variation slug, then `--`, and finally a hash. + * + * See `wp_create_block_style_variation_instance_name` for class generation. + */ + preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches ); + + if ( empty( $matches ) ) { + return $block_content; + } + + $tags = new WP_HTML_Tag_Processor( $block_content ); + + if ( $tags->next_tag() ) { + /* + * Ensure the variation instance class name set in the + * `render_block_data` filter is applied in markup. + * See `wp_render_block_style_variation_support_styles`. + */ + $tags->add_class( $matches[0] ); + } + + return $tags->get_updated_html(); +} + +/** + * Collects block style variation data for merging with theme.json data. + * As each block style variation is processed it is registered if it hasn't + * been already. This registration is required for later sanitization of + * theme.json data. + * + * @since 6.6.0 + * @access private + * + * @param array $variations Shared block style variations. + * + * @return array Block variations data to be merged under `styles.blocks`. + */ +function wp_resolve_and_register_block_style_variations( $variations ) { + $variations_data = array(); + + if ( empty( $variations ) ) { + return $variations_data; + } + + $registry = WP_Block_Styles_Registry::get_instance(); + $have_named_variations = ! wp_is_numeric_array( $variations ); + + foreach ( $variations as $key => $variation ) { + $supported_blocks = $variation['blockTypes'] ?? array(); + + /* + * Standalone theme.json partial files for block style variations + * will have their styles under a top-level property by the same name. + * Variations defined within an existing theme.json or theme style + * variation will themselves already be the required styles data. + */ + $variation_data = $variation['styles'] ?? $variation; + + if ( empty( $variation_data ) ) { + continue; + } + + /* + * Block style variations read in via standalone theme.json partials + * need to have their name set to the kebab case version of their title. + */ + $variation_name = $have_named_variations ? $key : _wp_to_kebab_case( $variation['title'] ); + $variation_label = $variation['title'] ?? $variation_name; + + foreach ( $supported_blocks as $block_type ) { + $registered_styles = $registry->get_registered_styles_for_block( $block_type ); + + // Register block style variation if it hasn't already been registered. + if ( ! array_key_exists( $variation_name, $registered_styles ) ) { + register_block_style( + $block_type, + array( + 'name' => $variation_name, + 'label' => $variation_label, + ) + ); + } + + // Add block style variation data under current block type. + $path = array( $block_type, 'variations', $variation_name ); + _wp_array_set( $variations_data, $path, $variation_data ); + } + } + + return $variations_data; +} + +/** + * Merges variations data with existing theme.json data ensuring that the + * current theme.json data values take precedence. + * + * @since 6.6.0 + * @access private + * + * @param array $variations_data Block style variations data keyed by block type. + * @param WP_Theme_JSON_Data $theme_json Current theme.json data. + * @param string $origin Origin for the theme.json data. + * + * @return WP_Theme_JSON The merged theme.json data. + */ +function wp_merge_block_style_variations_data( $variations_data, $theme_json, $origin = 'theme' ) { + if ( empty( $variations_data ) ) { + return $theme_json; + } + + $variations_theme_json_data = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( 'blocks' => $variations_data ), + ); + + $variations_theme_json = new WP_Theme_JSON_Data( $variations_theme_json_data, $origin ); + + /* + * Merge the current theme.json data over shared variation data so that + * any explicit per block variation values take precedence. + */ + return $variations_theme_json->update_with( $theme_json->get_data() ); +} + +/** + * Merges any shared block style variation definitions from a theme style + * variation into their appropriate block type within theme json styles. Any + * custom user selections already made will take precedence over the shared + * style variation value. + * + * @since 6.6.0 + * @access private + * + * @param WP_Theme_JSON_Data $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data + */ +function wp_resolve_block_style_variations_from_theme_style_variation( $theme_json ) { + $theme_json_data = $theme_json->get_data(); + $shared_variations = $theme_json_data['styles']['blocks']['variations'] ?? array(); + $variations_data = wp_resolve_and_register_block_style_variations( $shared_variations ); + + return wp_merge_block_style_variations_data( $variations_data, $theme_json, 'user' ); +} + +/** + * Merges block style variation data sourced from standalone partial + * theme.json files. + * + * @since 6.6.0 + * @access private + * + * @param WP_Theme_JSON_Data $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data + */ +function wp_resolve_block_style_variations_from_theme_json_partials( $theme_json ) { + $block_style_variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' ); + $variations_data = wp_resolve_and_register_block_style_variations( $block_style_variations ); + + return wp_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Merges shared block style variations registered within the + * `styles.blocks.variations` property of the primary theme.json file. + * + * @since 6.6.0 + * @access private + * + * @param WP_Theme_JSON_Data $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data + */ +function wp_resolve_block_style_variations_from_primary_theme_json( $theme_json ) { + $theme_json_data = $theme_json->get_data(); + $block_style_variations = $theme_json_data['styles']['blocks']['variations'] ?? array(); + $variations_data = wp_resolve_and_register_block_style_variations( $block_style_variations ); + + return wp_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Merges block style variations registered via the block styles registry with a + * style object, under their appropriate block types within theme.json styles. + * Any variation values defined within the theme.json specific to a block type + * will take precedence over these shared definitions. + * + * @since 6.6.0 + * @access private + * + * @param WP_Theme_JSON_Data $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data + */ +function wp_resolve_block_style_variations_from_styles_registry( $theme_json ) { + $registry = WP_Block_Styles_Registry::get_instance(); + $styles = $registry->get_all_registered(); + $variations_data = array(); + + foreach ( $styles as $block_type => $variations ) { + foreach ( $variations as $variation_name => $variation ) { + if ( ! empty( $variation['style_data'] ) ) { + $path = array( $block_type, 'variations', $variation_name ); + _wp_array_set( $variations_data, $path, $variation['style_data'] ); + } + } + } + + return wp_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Enqueues styles for block style variations. + * + * @since 6.6.0 + * @access private + */ +function wp_enqueue_block_style_variation_styles() { + wp_enqueue_style( 'block-style-variation-styles' ); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( 'block-style-variation', array() ); + +add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 ); +add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 ); +add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 ); + +// Resolve block style variations from all their potential sources. The order here is deliberate. +add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_primary_theme_json', 10, 1 ); +add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_theme_json_partials', 10, 1 ); +add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_styles_registry', 10, 1 ); + +add_filter( 'wp_theme_json_data_user', 'wp_resolve_block_style_variations_from_theme_style_variation', 10, 1 ); diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 3ad2df5b4040d..814b291487352 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -701,16 +701,46 @@ private static function recursively_iterate_json( $dir ) { return $nested_json_files; } + /** + * Determines if a supplied style variation matches the provided scope. + * + * For backwards compatibility, if a variation does not define any scope + * related property, e.g. `blockTypes`, it is assumed to be a theme style + * variation. + * + * @since 6.6.0 + * + * @param array $variation Theme.json shaped style variation object. + * @param string $scope Scope to check e.g. theme, block etc. + * + * @return boolean + */ + private static function style_variation_has_scope( $variation, $scope ) { + if ( 'block' === $scope ) { + return isset( $variation['blockTypes'] ); + } + + if ( 'theme' === $scope ) { + return ! isset( $variation['blockTypes'] ); + } + + return false; + } /** * Returns the style variations defined by the theme. * * @since 6.0.0 * @since 6.2.0 Returns parent theme variations if theme is a child. + * @since 6.6.0 Added configurable scope parameter to allow filtering + * theme.json partial files by the scope to which they + * can be applied e.g. theme vs block etc. + * + * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc. * * @return array */ - public static function get_style_variations() { + public static function get_style_variations( $scope = 'theme' ) { $variation_files = array(); $variations = array(); $base_directory = get_stylesheet_directory() . '/styles'; @@ -733,7 +763,7 @@ public static function get_style_variations() { ksort( $variation_files ); foreach ( $variation_files as $path => $file ) { $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { + if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) { $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); $variation = ( new WP_Theme_JSON( $translated ) )->get_raw_data(); if ( empty( $variation['title'] ) ) { diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 61d1891322489..bdafc03863a3b 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -346,9 +346,11 @@ class WP_Theme_JSON { * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, * added the `customTemplates` and `templateParts` values. * @since 6.3.0 Added the `description` value. + * @since 6.6.0 Added `blockTypes` to support block style variation theme.json partials. * @var string[] */ const VALID_TOP_LEVEL_KEYS = array( + 'blockTypes', 'customTemplates', 'description', 'patterns', @@ -823,6 +825,7 @@ protected static function do_opt_in_into_settings( &$context ) { * @since 5.8.0 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. * @since 6.3.0 Added the `$valid_variations` parameter. + * @since 6.6.0 Updated schema to allow extended block style variations. * * @param array $input Structure to sanitize. * @param array $valid_block_names List of valid block names. @@ -881,6 +884,27 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks = array(); $schema_settings_blocks = array(); + + /* + * Generate a schema for blocks. + * - Block styles can contain `elements` & `variations` definitions. + * - Variations definitions cannot be nested. + * - Variations can contain styles for inner `blocks`. + * - Variation inner `blocks` styles can contain `elements`. + * + * As each variation needs a `blocks` schema but further nested + * inner `blocks`, the overall schema will be generated in multiple passes. + */ + 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; + } + + $block_style_variation_styles = static::VALID_STYLES; + $block_style_variation_styles['blocks'] = $schema_styles_blocks; + $block_style_variation_styles['elements'] = $schema_styles_elements; + foreach ( $valid_block_names as $block ) { // Build the schema for each block style variation. $style_variation_names = array(); @@ -897,12 +921,9 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_variations = array(); if ( ! empty( $style_variation_names ) ) { - $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level ); + $schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles ); } - $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; } @@ -913,6 +934,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema['settings']['blocks'] = $schema_settings_blocks; $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); + /* + * Shared block style variations can be registered from the theme.json data so we can't + * validate them against pre-registered block style variations. + */ + $schema['styles']['blocks']['variations'] = null; + // Remove anything that's not present in the schema. foreach ( array( 'styles', 'settings' ) as $subtree ) { if ( ! isset( $input[ $subtree ] ) ) { @@ -1016,16 +1043,36 @@ protected static function prepend_to_selector( $selector, $to_prepend ) { * @since 5.9.0 Added `duotone` key with CSS selector. * @since 6.1.0 Added `features` key with block support feature level selectors. * @since 6.3.0 Refactored and stabilized selectors API. + * @since 6.6.0 Updated to include block style variations from the block styles registry. * * @return array Block metadata. */ protected static function get_blocks_metadata() { - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + $style_registry = WP_Block_Styles_Registry::get_instance(); // Is there metadata for all currently registered blocks? $blocks = array_diff_key( $blocks, static::$blocks_metadata ); if ( empty( $blocks ) ) { + /* + * New block styles may have been registered within WP_Block_Styles_Registry. + * Update block metadata for any new block style variations. + */ + $registered_styles = $style_registry->get_all_registered(); + foreach ( static::$blocks_metadata as $block_name => $block_metadata ) { + if ( ! empty( $registered_styles[ $block_name ] ) ) { + $style_selectors = $block_metadata['styleVariations'] ?? array(); + + foreach ( $registered_styles[ $block_name ] as $block_style ) { + if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) { + $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] ); + } + } + + static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; + } + } return static::$blocks_metadata; } @@ -1060,11 +1107,20 @@ protected static function get_blocks_metadata() { } // If the block has style variations, append their selectors to the block metadata. + $style_selectors = array(); if ( ! empty( $block_type->styles ) ) { - $style_selectors = array(); foreach ( $block_type->styles as $style ) { $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); } + } + + // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json. + $registered_styles = $style_registry->get_registered_styles_for_block( $block_name ); + foreach ( $registered_styles as $style ) { + $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + } + + if ( ! empty( $style_selectors ) ) { static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } } @@ -1158,6 +1214,7 @@ public function get_settings() { * @since 5.8.0 * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters. * @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available. + * @since 6.6.0 Added `skip_root_layout_styles` option to omit layout styles if desired. * * @param string[] $types Types of styles to load. Will load all by default. It accepts: * - `variables`: only the CSS Custom Properties for presets & custom ones. @@ -1165,9 +1222,10 @@ public function get_settings() { * - `presets`: only the classes for the presets. * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS. * @param array $options An array of options for now used for internal purposes only (may change without notice). - * The options currently supported are 'scope' that makes sure all style are scoped to a - * given selector, and root_selector which overwrites and forces a given selector to be - * used on the root node. + * The options currently supported are: + * - 'scope' that makes sure all style are scoped to a given selector + * - `root_selector` which overwrites and forces a given selector to be used on the root node + * - `skip_root_layout_styles` which omits root layout styles from the generated stylesheet. * @return string The resulting stylesheet. */ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { @@ -1220,7 +1278,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' } if ( in_array( 'styles', $types, true ) ) { - if ( false !== $root_style_key ) { + if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) { $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); } $stylesheet .= $this->get_block_classes( $style_nodes ); @@ -3114,6 +3172,7 @@ protected static function filter_slugs( $node, $slugs ) { * * @since 5.9.0 * @since 6.3.2 Preserves global styles block variations when securing styles. + * @since 6.6.0 Updated to allow variation element styles. * * @param array $theme_json Structure to sanitize. * @return array Sanitized structure. @@ -3175,6 +3234,29 @@ public static function remove_insecure_properties( $theme_json ) { } $variation_output = static::remove_insecure_styles( $variation_input ); + + // Process a variation's elements and element pseudo selector styles. + if ( isset( $variation_input['elements'] ) ) { + foreach ( $valid_element_names as $element_name ) { + $element_input = $variation_input['elements'][ $element_name ] ?? null; + if ( $element_input ) { + $element_output = static::remove_insecure_styles( $element_input ); + + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { + if ( isset( $element_input[ $pseudo_selector ] ) ) { + $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] ); + } + } + } + + if ( ! empty( $element_output ) ) { + _wp_array_set( $variation_output, array( 'elements', $element_name ), $element_output ); + } + } + } + } + if ( ! empty( $variation_output ) ) { _wp_array_set( $sanitized, $variation['path'], $variation_output ); } diff --git a/src/wp-settings.php b/src/wp-settings.php index 9ea496aa11f10..8467707abe601 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -366,6 +366,7 @@ require ABSPATH . WPINC . '/block-supports/utils.php'; require ABSPATH . WPINC . '/block-supports/align.php'; require ABSPATH . WPINC . '/block-supports/background.php'; +require ABSPATH . WPINC . '/block-supports/block-style-variations.php'; require ABSPATH . WPINC . '/block-supports/border.php'; require ABSPATH . WPINC . '/block-supports/colors.php'; require ABSPATH . WPINC . '/block-supports/custom-classname.php'; diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css new file mode 100644 index 0000000000000..c1cc20aaf1f10 --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css @@ -0,0 +1,8 @@ +/* +Theme Name: Block Theme Child With Block Style Variations Theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Template: block-theme +Version: 1.0.0 +Text Domain: block-theme-child-with-block-style-variations +*/ diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json new file mode 100644 index 0000000000000..195321a33b336 --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns", "core/media-text" ], + "styles": { + "color": { + "background": "darkcyan", + "text": "aliceblue" + } + } +} diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json new file mode 100644 index 0000000000000..a471d8f326a4a --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 3 +} diff --git a/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json b/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json new file mode 100644 index 0000000000000..356bc4fc3de7d --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "indigo", + "text": "plum" + } + } +} diff --git a/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json b/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json new file mode 100644 index 0000000000000..8b79948517255 --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "midnightblue", + "text": "lightblue" + } + } +} diff --git a/tests/phpunit/tests/block-supports/block-style-variations.php b/tests/phpunit/tests/block-supports/block-style-variations.php new file mode 100644 index 0000000000000..467144ffdc781 --- /dev/null +++ b/tests/phpunit/tests/block-supports/block-style-variations.php @@ -0,0 +1,132 @@ +theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + + // Reset data between tests. + wp_clean_theme_json_cache(); + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + /** + * Tests that block style variations registered via either + * `register_block_style` with a style object, or a standalone block style + * variation file within `/styles`, are added to the theme data. + * + * @ticket 61312 + */ + public function test_add_registered_block_styles_to_theme_data() { + switch_theme( 'block-theme' ); + + $variation_styles_data = array( + 'color' => array( + 'background' => 'darkslateblue', + 'text' => 'lavender', + ), + 'blocks' => array( + 'core/heading' => array( + 'color' => array( + 'text' => 'violet', + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'fuchsia', + ), + ':hover' => array( + 'color' => array( + 'text' => 'deeppink', + ), + ), + ), + ), + ); + + register_block_style( + 'core/group', + array( + 'name' => 'my-variation', + 'style_data' => $variation_styles_data, + ) + ); + + $theme_json = WP_Theme_JSON_Resolver::get_theme_data()->get_raw_data(); + $group_styles = $theme_json['styles']['blocks']['core/group'] ?? array(); + $expected = array( + 'variations' => array( + 'my-variation' => $variation_styles_data, + + /* + * The following block style variations are registered + * automatically from their respective JSON files within the + * theme's `/styles` directory. + */ + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'indigo', + 'text' => 'plum', + ), + ), + 'block-style-variation-b' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', + ), + ), + ), + ); + + unregister_block_style( 'core/group', 'my-variation' ); + + $this->assertSameSetsWithIndex( $group_styles, $expected ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpCreateBlockStyleVariationInstanceName.php b/tests/phpunit/tests/block-supports/wpCreateBlockStyleVariationInstanceName.php new file mode 100644 index 0000000000000..1e37d48e8ed1d --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpCreateBlockStyleVariationInstanceName.php @@ -0,0 +1,24 @@ + 'test/block' ); + $actual = wp_create_block_style_variation_instance_name( $block, 'my-variation' ); + $expected = 'my-variation--' . md5( serialize( $block ) ); + + $this->assertSame( $expected, $actual, 'Block style variation instance name should be correct' ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpGetBlockStyleVariationNameFromClass.php b/tests/phpunit/tests/block-supports/wpGetBlockStyleVariationNameFromClass.php new file mode 100644 index 0000000000000..d1bf080e18639 --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpGetBlockStyleVariationNameFromClass.php @@ -0,0 +1,65 @@ +assertSame( + $expected, + $actual, + 'Block style variation names extracted from CSS class string should match' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_block_style_variation_name_extraction() { + return array( + // @ticket 61312 + 'missing class string' => array( + 'class_string' => null, + 'expected' => null, + ), + // @ticket 61312 + 'empty class string' => array( + 'class_string' => '', + 'expected' => array(), + ), + // @ticket 61312 + 'no variation' => array( + 'class_string' => 'is-style no-variation', + 'expected' => array(), + ), + // @ticket 61312 + 'single variation' => array( + 'class_string' => 'custom-class is-style-outline', + 'expected' => array( 'outline' ), + ), + // @ticket 61312 + 'multiple variations' => array( + 'class_string' => 'is-style-light custom-class is-style-outline', + 'expected' => array( 'light', 'outline' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/theme/themeDir.php b/tests/phpunit/tests/theme/themeDir.php index 1d6fb45db3341..a953a04bc5533 100644 --- a/tests/phpunit/tests/theme/themeDir.php +++ b/tests/phpunit/tests/theme/themeDir.php @@ -177,6 +177,7 @@ public function test_theme_list() { 'Block Theme', 'Block Theme Child Theme', 'Block Theme Child Deprecated Path', + 'Block Theme Child With Block Style Variations Theme', 'Block Theme Child with no theme.json', 'Block Theme Child Theme With Fluid Layout', 'Block Theme Child Theme With Fluid Typography', diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 52cd20641f45e..581828ba6f29c 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -1023,97 +1023,146 @@ public function data_get_merged_data_returns_origin() { } /** - * Tests that get_style_variations returns all variations, including parent theme variations if the theme is a child, - * and that the child variation overwrites the parent variation of the same name. + * Tests that `get_style_variations` returns all the appropriate variations, + * including parent variations if the theme is a child, and that the child + * variation overwrites the parent variation of the same name. + * + * Note: This covers both theme and block style variations. * * @ticket 57545 + * @ticket 61312 * * @covers WP_Theme_JSON_Resolver::get_style_variations + * + * @dataProvider data_get_style_variations + * + * @param string $theme Name of the theme to use. + * @param string $scope Scope to filter variations by e.g. theme vs block. + * @param array $expected_variations Collection of expected variations. */ - public function test_get_style_variations_returns_all_variations() { - // Switch to a child theme. - switch_theme( 'block-theme-child' ); + public function test_get_style_variations( $theme, $scope, $expected_variations ) { + switch_theme( $theme ); wp_set_current_user( self::$administrator_id ); - $actual_settings = WP_Theme_JSON_Resolver::get_style_variations(); - $expected_settings = array( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'title' => 'variation-a', - 'settings' => array( - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'dark', - 'name' => 'Dark', - 'color' => '#010101', + $actual_variations = WP_Theme_JSON_Resolver::get_style_variations( $scope ); + + wp_recursive_ksort( $actual_variations ); + wp_recursive_ksort( $expected_variations ); + + $this->assertSame( $expected_variations, $actual_variations ); + } + + /** + * Data provider for test_get_style_variations + * + * @return array + */ + public function data_get_style_variations() { + return array( + // @ticket 57545 + 'theme_style_variations' => array( + 'theme' => 'block-theme-child', + 'scope' => 'theme', + 'expected_variations' => array( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'variation-a', + 'settings' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), ), ), ), ), ), ), - ), - ), - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'title' => 'variation-b', - 'settings' => array( - 'blocks' => array( - 'core/post-title' => array( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'variation-b', + 'settings' => array( + 'blocks' => array( + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), + ), + ), + ), + ), + ), + ), + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'Block theme variation', + 'settings' => array( 'color' => array( 'palette' => array( 'theme' => array( array( - 'slug' => 'dark', - 'name' => 'Dark', - 'color' => '#010101', + 'slug' => 'foreground', + 'name' => 'Foreground', + 'color' => '#3F67C6', ), ), ), ), ), + 'styles' => array( + 'blocks' => array( + 'core/post-title' => array( + 'typography' => array( + 'fontWeight' => '700', + ), + ), + ), + ), ), ), ), - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'title' => 'Block theme variation', - 'settings' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'foreground', - 'name' => 'Foreground', - 'color' => '#3F67C6', - ), + 'block_style_variations' => array( + 'theme' => 'block-theme-child-with-block-style-variations', + 'scope' => 'block', + 'expected_variations' => array( + array( + 'blockTypes' => array( 'core/group', 'core/columns', 'core/media-text' ), + 'version' => 3, + 'title' => 'block-style-variation-a', + 'styles' => array( + 'color' => array( + 'background' => 'darkcyan', + 'text' => 'aliceblue', ), ), ), - ), - 'styles' => array( - 'blocks' => array( - 'core/post-title' => array( - 'typography' => array( - 'fontWeight' => '700', + array( + 'blockTypes' => array( 'core/group', 'core/columns' ), + 'version' => 3, + 'title' => 'block-style-variation-b', + 'styles' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', ), ), ), ), ), ); - - wp_recursive_ksort( $actual_settings ); - wp_recursive_ksort( $expected_settings ); - - $this->assertSame( - $expected_settings, - $actual_settings - ); } /**