diff --git a/wp-includes/block-supports/block-style-variations.php b/wp-includes/block-supports/block-style-variations.php new file mode 100644 index 0000000000..c8ba6e75aa --- /dev/null +++ b/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/wp-includes/class-wp-theme-json-resolver.php b/wp-includes/class-wp-theme-json-resolver.php index 3ad2df5b40..814b291487 100644 --- a/wp-includes/class-wp-theme-json-resolver.php +++ b/wp-includes/class-wp-theme-json-resolver.php @@ -701,16 +701,46 @@ class WP_Theme_JSON_Resolver { 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 @@ class WP_Theme_JSON_Resolver { 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/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php index 61d1891322..bdafc03863 100644 --- a/wp-includes/class-wp-theme-json.php +++ b/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 @@ class WP_Theme_JSON { * @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 @@ class WP_Theme_JSON { $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 @@ class WP_Theme_JSON { $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 @@ class WP_Theme_JSON { $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 @@ class WP_Theme_JSON { * @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 @@ class WP_Theme_JSON { } // 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 @@ class WP_Theme_JSON { * @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 @@ class WP_Theme_JSON { * - `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 @@ class WP_Theme_JSON { } 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 @@ class WP_Theme_JSON { * * @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 @@ class WP_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/wp-includes/version.php b/wp-includes/version.php index 7a834bae24..b620501345 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.6-alpha-58263'; +$wp_version = '6.6-alpha-58264'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-settings.php b/wp-settings.php index 9ea496aa11..8467707abe 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -366,6 +366,7 @@ require ABSPATH . WPINC . '/class-wp-block-supports.php'; 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';