diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php index 3942fed24b98a8..2ea65cd5f11ca6 100644 --- a/lib/block-supports/block-style-variations.php +++ b/lib/block-supports/block-style-variations.php @@ -102,6 +102,7 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) } $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data(); + $tree = WP_Theme_JSON_Resolver_Gutenberg::resolve_theme_file_uris( $tree ); $theme_json = $tree->get_raw_data(); // Only the first block style variation with data is supported. @@ -172,6 +173,7 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) $styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) ); $variation_theme_json = new WP_Theme_JSON_Gutenberg( $config, 'blocks' ); + $variation_theme_json = WP_Theme_JSON_Resolver_Gutenberg::resolve_theme_file_uris( $variation_theme_json ); $variation_styles = $variation_theme_json->get_stylesheet( array( 'styles' ), array( 'custom' ), diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index dafa8b25f278fc..66b7cec90cd8fc 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -2602,6 +2602,7 @@ protected static function get_setting_nodes( $theme_json, $selectors = array() ) * @param array $options An array of options to facilitate filtering style node generation * The options currently supported are: * - `include_block_style_variations` which includes CSS for block style variations. + * - `include_node_paths_only` which skips the selector generation. * @return array An array of style nodes metadata. */ protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) { @@ -2610,11 +2611,16 @@ protected static function get_style_nodes( $theme_json, $selectors = array(), $o return $nodes; } + $include_node_paths_only = $options['include_node_paths_only'] ?? false; + // Top-level. - $nodes[] = array( - 'path' => array( 'styles' ), - 'selector' => static::ROOT_BLOCK_SELECTOR, + $node = array( + 'path' => array( 'styles' ), ); + if ( ! $include_node_paths_only ) { + $node['selector'] = static::ROOT_BLOCK_SELECTOR; + } + $nodes[] = $node; if ( isset( $theme_json['styles']['elements'] ) ) { foreach ( self::ELEMENTS as $element => $selector ) { @@ -2623,20 +2629,25 @@ protected static function get_style_nodes( $theme_json, $selectors = array(), $o } // Handle element defaults. - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::ELEMENTS[ $element ], + $node = array( + 'path' => array( 'styles', 'elements', $element ), ); + if ( ! $include_node_paths_only ) { + $node['selector'] = static::ELEMENTS[ $element ]; + } + $nodes[] = $node; // Handle any pseudo selectors for the element. if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), + $node = array( + 'path' => array( 'styles', 'elements', $element ), ); + if ( ! $include_node_paths_only ) { + $node['selector'] = static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ); + } + $nodes[] = $node; } } } @@ -2676,6 +2687,26 @@ public function get_styles_block_nodes() { return static::get_block_nodes( $this->theme_json ); } + /** + * A public helper to get all style node paths in theme.json, + * including elements and block styles variations. This is useful + * when iterating over all style nodes to search for or replace any values. + * + * @since 6.8.0 + * + * @return array An array of paths to style nodes in theme.json. + */ + public function get_styles_nodes_paths() { + return static::get_style_nodes( + $this->theme_json, + array(), + array( + 'include_node_paths_only' => true, + 'include_block_style_variations' => true, + ) + ); + } + /** * Returns a filtered declarations array if there is a separator block with only a background * style defined in theme.json by adding a color attribute to reflect the changes in the front. @@ -2752,6 +2783,21 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt $nodes[] = array( 'path' => $node_path, ); + + if ( $include_variations && isset( $node['variations'] ) ) { + foreach ( $node['variations'] as $variation => $node ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), + ); + if ( isset( $node['blocks'] ) ) { + foreach ( $node['blocks'] as $variation_block_name => $node ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation, 'blocks', $variation_block_name ), + ); + } + } + } + } } else { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index cd02b5a45c22f7..ea83aa6939cfc7 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -855,53 +855,35 @@ public static function get_resolved_theme_uris( $theme_json ) { // Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri. $placeholder = 'file:./'; - // Top level styles. - $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null; - if ( - isset( $background_image_url ) && - is_string( $background_image_url ) && - // Skip if the src doesn't start with the placeholder, as there's nothing to replace. - str_starts_with( $background_image_url, $placeholder ) ) { + /* + * Style values are merged at the leaf level, however + * some values provide exceptions, namely style values that are + * objects and represent unique definitions for the style. + */ + $style_nodes = $theme_json->get_styles_nodes_paths(); + + foreach ( $style_nodes as $style_node ) { + $path = $style_node['path']; + $background_image_path = array_merge( $path, array( 'background', 'backgroundImage', 'url' ) ); + $background_image_url = _wp_array_get( $theme_json_data, $background_image_path, null ); + if ( + isset( $background_image_url ) && + is_string( $background_image_url ) && + // Skip if the src doesn't start with the placeholder, as there's nothing to replace. + str_starts_with( $background_image_url, $placeholder ) ) { $file_type = wp_check_filetype( $background_image_url ); $src_url = str_replace( $placeholder, '', $background_image_url ); $resolved_theme_uri = array( 'name' => $background_image_url, 'href' => sanitize_url( get_theme_file_uri( $src_url ) ), - 'target' => 'styles.background.backgroundImage.url', + 'target' => implode( '.', $background_image_path ), ); if ( isset( $file_type['type'] ) ) { $resolved_theme_uri['type'] = $file_type['type']; } $resolved_theme_uris[] = $resolved_theme_uri; - } - - // Block styles. - if ( ! empty( $theme_json_data['styles']['blocks'] ) ) { - foreach ( $theme_json_data['styles']['blocks'] as $block_name => $block_styles ) { - if ( ! isset( $block_styles['background']['backgroundImage']['url'] ) ) { - continue; - } - $background_image_url = $block_styles['background']['backgroundImage']['url'] ?? null; - if ( - isset( $background_image_url ) && - is_string( $background_image_url ) && - // Skip if the src doesn't start with the placeholder, as there's nothing to replace. - str_starts_with( $background_image_url, $placeholder ) ) { - $file_type = wp_check_filetype( $background_image_url ); - $src_url = str_replace( $placeholder, '', $background_image_url ); - $resolved_theme_uri = array( - 'name' => $background_image_url, - 'href' => sanitize_url( get_theme_file_uri( $src_url ) ), - 'target' => "styles.blocks.{$block_name}.background.backgroundImage.url", - ); - if ( isset( $file_type['type'] ) ) { - $resolved_theme_uri['type'] = $file_type['type']; - } - $resolved_theme_uris[] = $resolved_theme_uri; - } } } - return $resolved_theme_uris; } diff --git a/packages/block-editor/src/hooks/block-style-variation.js b/packages/block-editor/src/hooks/block-style-variation.js index 65582d0c0cf948..ce174e4cf1332b 100644 --- a/packages/block-editor/src/hooks/block-style-variation.js +++ b/packages/block-editor/src/hooks/block-style-variation.js @@ -16,7 +16,10 @@ import { import { usePrivateStyleOverride } from './utils'; import { getValueFromObjectPath } from '../utils/object'; import { store as blockEditorStore } from '../store'; -import { globalStylesDataKey } from '../store/private-keys'; +import { + globalStylesDataKey, + globalStylesLinksDataKey, +} from '../store/private-keys'; import { unlock } from '../lock-unlock'; const VARIATION_PREFIX = 'is-style-'; @@ -86,7 +89,6 @@ export function __unstableBlockStyleVariationOverridesWithConfig( { config } ) { [] ); const { getBlockName } = useSelect( blockEditorStore ); - const overridesWithConfig = useMemo( () => { if ( ! overrides?.length ) { return; @@ -257,13 +259,17 @@ function useBlockStyleVariation( name, variation, clientId ) { // if in the site editor. Otherwise fall back to whatever is in the // editor settings and available in the post editor. const { merged: mergedConfig } = useContext( GlobalStylesContext ); - const { globalSettings, globalStyles } = useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - return { - globalSettings: settings.__experimentalFeatures, - globalStyles: settings[ globalStylesDataKey ], - }; - }, [] ); + const { globalSettings, globalStyles, globalLinks } = useSelect( + ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + return { + globalSettings: settings.__experimentalFeatures, + globalStyles: settings[ globalStylesDataKey ], + globalLinks: settings[ globalStylesLinksDataKey ], + }; + }, + [] + ); return useMemo( () => { const variationStyles = getVariationStylesWithRefValues( @@ -289,11 +295,13 @@ function useBlockStyleVariation( name, variation, clientId ) { }, }, }, + _links: mergedConfig?._links ?? globalLinks, }; }, [ mergedConfig, globalSettings, globalStyles, + globalLinks, variation, clientId, name, @@ -310,7 +318,7 @@ function useBlockProps( { name, className, clientId } ) { const variation = getVariationNameFromClass( className, registeredStyles ); const variationClass = `${ VARIATION_PREFIX }${ variation }-${ clientId }`; - const { settings, styles } = useBlockStyleVariation( + const { settings, styles, _links } = useBlockStyleVariation( name, variation, clientId @@ -321,7 +329,7 @@ function useBlockProps( { name, className, clientId } ) { return; } - const variationConfig = { settings, styles }; + const variationConfig = { settings, styles, _links }; const blockSelectors = getBlockSelectors( getBlockTypes(), getBlockStyles, @@ -349,7 +357,7 @@ function useBlockProps( { name, className, clientId } ) { variationStyles: true, } ); - }, [ variation, settings, styles, getBlockStyles, clientId ] ); + }, [ variation, settings, styles, _links, getBlockStyles, clientId ] ); usePrivateStyleOverride( { id: `variation-${ clientId }`, diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index d2339f2496290c..c8e6c33fa3bf71 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -1293,6 +1293,15 @@ public function test_get_resolved_theme_uris() { 'url' => 'file:./example/img/image.png', ), ), + 'elements' => array( + 'button' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./example/img/button.png', + ), + ), + ), + ), 'blocks' => array( 'core/quote' => array( 'background' => array( @@ -1308,6 +1317,22 @@ public function test_get_resolved_theme_uris() { ), ), ), + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./example/img/group.gif', + ), + ), + 'elements' => array( + 'button' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./example/img/group-button.png', + ), + ), + ), + ), + ), ), ), ) @@ -1320,6 +1345,12 @@ public function test_get_resolved_theme_uris() { 'target' => 'styles.background.backgroundImage.url', 'type' => 'image/png', ), + array( + 'name' => 'file:./example/img/button.png', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/button.png', + 'target' => 'styles.elements.button.background.backgroundImage.url', + 'type' => 'image/png', + ), array( 'name' => 'file:./example/img/quote.jpg', 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/quote.jpg', @@ -1332,6 +1363,18 @@ public function test_get_resolved_theme_uris() { 'target' => 'styles.blocks.core/verse.background.backgroundImage.url', 'type' => 'image/gif', ), + array( + 'name' => 'file:./example/img/group.gif', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/group.gif', + 'target' => 'styles.blocks.core/group.background.backgroundImage.url', + 'type' => 'image/gif', + ), + array( + 'name' => 'file:./example/img/group-button.png', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/group-button.png', + 'target' => 'styles.blocks.core/group.elements.button.background.backgroundImage.url', + 'type' => 'image/png', + ), ); /* @@ -1350,6 +1393,62 @@ public function test_get_resolved_theme_uris() { $this->assertSame( $expected_data, $actual ); } + public function test_get_resolved_theme_uris_in_variations() { + register_block_style( + 'core/group', + array( + 'name' => 'group-background-twinky', + 'label' => 'Group block twinky background', + 'style_data' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./example/img/group-twinky.png', + ), + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./example/img/group-fonzis.png', + ), + ), + ), + ), + ), + ) + ); + $theme_json = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data(); + $expected_data = array( + array( + 'name' => 'file:./example/img/group-twinky.png', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/group-twinky.png', + 'target' => 'styles.blocks.core/group.variations.group-background-twinky.background.backgroundImage.url', + 'type' => 'image/png', + ), + array( + 'name' => 'file:./example/img/group-fonzis.png', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/group-fonzis.png', + 'target' => 'styles.blocks.core/group.variations.group-background-twinky.blocks.core/group.background.backgroundImage.url', + 'type' => 'image/png', + ), + ); + /* + * This filter callback normalizes the return value from `get_theme_file_uri` + * to guard against changes in test environments. + * The test suite otherwise returns full system dir path, e.g., + * /wordpress-phpunit/includes/../data/themedir1/default/example/img/image.png + */ + $filter_theme_file_uri_callback = function ( $file ) { + return 'https://example.org/wp-content/themes/example-theme/example/' . explode( 'example/', $file )[1]; + }; + add_filter( 'theme_file_uri', $filter_theme_file_uri_callback ); + $actual = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $theme_json ); + remove_filter( 'theme_file_uri', $filter_theme_file_uri_callback ); + + $this->assertSame( $expected_data, $actual ); + unregister_block_style( 'core/group', 'group-background-twinky' ); + } + /** * Tests that block style variations data gets merged in the following * priority order, from highest priority to lowest.