From 3153e0e11d54cb8d2d1231ef90566da276c9073c Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:06:38 +1000 Subject: [PATCH 01/13] Add styles.blocks.variations to theme.json schema --- schemas/json/theme.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 30016a16c38f9d..cb709ccb672923 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -2164,6 +2164,9 @@ }, "core/widget-group": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + }, + "variations": { + "$ref": "#/definitions/stylesVariationsPropertiesComplete" } }, "patternProperties": { @@ -2194,7 +2197,7 @@ "$ref": "#/definitions/stylesElementsPropertiesComplete" }, "variations": { - "$ref": "#/definitions/stylesVariationsPropertiesComplete" + "$ref": "#/definitions/stylesVariationsPropertiesAndRefComplete" } }, "additionalProperties": false @@ -2209,6 +2212,21 @@ } } }, + "stylesVariationsPropertiesAndRefComplete": { + "type": "object", + "patternProperties": { + "^[a-z][a-z0-9-]*$": { + "oneOf": [ + { + "$ref": "#/definitions/refComplete" + }, + { + "$ref": "#/definitions/stylesVariationPropertiesComplete" + } + ] + } + } + }, "stylesVariationPropertiesComplete": { "type": "object", "allOf": [ From 8b985b7e64a1090802a120bf927fb4c412aa171a Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:57:47 +1000 Subject: [PATCH 02/13] Process theme.json referenced block style variations --- lib/class-wp-theme-json-gutenberg.php | 106 +++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 58d29c14e7c025..e54bdc3023e52d 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -862,6 +862,26 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $block_style_variation_styles['blocks'] = $schema_styles_blocks; $block_style_variation_styles['elements'] = $schema_styles_elements; + // Generate schema for shared variations i.e. those that can be + // referenced with block style variations and live under + // styles.blocks.variations. These variations could be any valid block + // style variation. The schema will differ in that these variations + // cannot reference another. + // + // NOTE: The size of the schema array is already very large given + // entries for each individual block. This is compounded when multiple + // variations need to added to the schema. Would multiple passes for + // validation offer any improvements? + $unique_variations = array_unique( + call_user_func_array( 'array_merge', array_values( $valid_variations ) ) + ); + $schema_shared_style_variations = array_fill_keys( $unique_variations, $block_style_variation_styles ); + + // Allow refs only within the individual block type variations properties. + // Assigning it before `$schema_shared_style_variations` would mean + // shared variations would allow `ref` properties. + $block_style_variation_styles['ref'] = null; + foreach ( $valid_block_names as $block ) { $style_variation_names = array(); @@ -890,6 +910,7 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema['styles'] = static::VALID_STYLES; $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['blocks']['variations'] = $schema_shared_style_variations; $schema['styles']['elements'] = $schema_styles_elements; $schema['settings'] = static::VALID_SETTINGS; $schema['settings']['blocks'] = $schema_settings_blocks; @@ -2451,6 +2472,15 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { 'selector' => $variation_selector, ); + if ( + isset( $variation_node['ref'] ) && + str_starts_with( $variation_node['ref'], 'styles.blocks.variations' ) + ) { + $variation_path = explode( '.', $variation_node['ref'] ); + $referenced_variation = _wp_array_get( $theme_json, $variation_path, array() ); + $variation_node = static::merge_styles( $referenced_variation, $variation_node ); + } + $variation_blocks = $variation_node['blocks'] ?? array(); $variation_elements = $variation_node['elements'] ?? array(); @@ -2538,6 +2568,30 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { return $nodes; } + /** + * Retrieves a style node along with any of its non-customized block style + * variation data. + * + * @param array $theme_json A theme.json structure to modify. + * @param array $path Path for the node to retrie. + * + * @return array Style node with merged block style variation data if appropriate. + */ + public static function get_style_node_with_referenced_variations( $theme_json, $path ) { + $node = _wp_array_get( $theme_json, $path, array() ); + $index = array_search( 'variations', $path, true ); + + // Get any referenced variation and merge any node values into that. + if ( false !== $index && isset( $path[ $index + 1 ] ) ) { + $variation_path = array( 'styles', 'blocks', 'variations' ); + $variation_path = array_merge( $variation_path, array_slice( $path, $index + 1 ) ); + $variation_node = _wp_array_get( $theme_json, $variation_path, array() ); + $node = static::merge_styles( $variation_node, $node ); + } + + return $node; + } + /** * Gets the CSS rules for a particular block from theme.json. * @@ -2548,7 +2602,7 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { * @return string Styles for the block. */ public function get_styles_for_block( $block_metadata ) { - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + $node = static::get_style_node_with_referenced_variations( $this->theme_json, $block_metadata['path'] ); $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; $selector = $block_metadata['selector']; $settings = $this->theme_json['settings'] ?? null; @@ -2559,7 +2613,21 @@ public function get_styles_for_block( $block_metadata ) { $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() ); + // Given block style variations can reference an entire object + // rather than a single value, any values in the variation + // node itself should override the referenced style variation's + // values. + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); + if ( + isset( $style_variation_node['ref'] ) && + str_starts_with( $style_variation_node['ref'], 'styles.blocks.variations' ) + ) { + $variation_path = explode( '.', $style_variation_node['ref'] ); + $referenced_variation = _wp_array_get( $this->theme_json, $variation_path, array() ); + $style_variation_node = static::merge_styles( $referenced_variation, $style_variation_node ); + } + + $style_variation_node = $style_variation_node ? $style_variation_node : array(); $clean_style_variation_selector = trim( $style_variation['selector'] ); // Generate any feature/subfeature style declarations for the current style variation. @@ -4043,4 +4111,38 @@ function ( $carry, $item ) { $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); return $theme_json; } + + /** + * Recursively merge style objects without merging leaf values. + * + * `array_merge_recursive` will end up merging style values if they are + * present in both style objects resulting in an array when the value + * should be a string. + * + * @param array $target_styles Target style data. + * @param array $source_styles Style data to merge into the target. + * + * @return array Merged style data + */ + public static function merge_styles( $target_styles, $source_styles ) { + if ( empty( $source_styles ) ) { + return $target_styles; + } + + if ( empty( $target_styles ) ) { + return $source_styles; + } + + $merged_styles = $target_styles; + + foreach ( $source_styles as $key => $value ) { + if ( is_array( $value ) && isset( $merged_styles[ $key ] ) && is_array( $merged_styles[ $key ] ) ) { + $merged_styles[ $key ] = static::merge_styles( $merged_styles[ $key ], $value ); + } else { + $merged_styles[ $key ] = $value; + } + } + + return $merged_styles; + } } From 278801f230837fa0684d3b8842148c4b16744f48 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:05:35 +1000 Subject: [PATCH 03/13] Output referenced variation styles in site editor Globals Styles --- .../global-styles/use-global-styles-output.js | 8 +++- .../src/components/global-styles/utils.js | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index b1f6f395d9861f..51655636f2d014 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -21,6 +21,7 @@ import { ROOT_BLOCK_SELECTOR, scopeSelector, appendToSelector, + getMergedVariation, } from './utils'; import { getBlockCSSSelector } from './get-block-css-selector'; import { @@ -672,9 +673,14 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { const variations = {}; Object.entries( node.variations ).forEach( - ( [ variationName, variation ] ) => { + ( [ variationName, variationNode ] ) => { + const variation = getMergedVariation( + variationNode, + tree + ); variations[ variationName ] = pickStyleKeys( variation ); + const variationSelector = blockSelectors[ blockName ].styleVariationSelectors[ variationName diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index 08e38592cfa865..e276245ddcf6dc 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -450,3 +450,45 @@ export function areGlobalStyleConfigsEqual( original, variation ) { fastDeepEqual( original?.settings, variation?.settings ) ); } + +// TODO: Is this the right place for these utils? +// They're only used in the useGlobalStylesOutput hook and the block screen in +// the site editor. The block editor's utils/object.js is exported. Do we want +// that? +export const isObject = ( item ) => + !! item && typeof item === 'object' && ! Array.isArray( item ); + +export const deepMerge = ( target, source ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + const getter = Object.getOwnPropertyDescriptor( source, key )?.get; + if ( typeof getter === 'function' ) { + Object.defineProperty( target, key, { get: getter } ); + } else if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +/** + * Retrieves any referenced block style variation data and overrides that with + * the current variation style object values. + * + * @param {Object} variation Style object for block style variation. + * @param {Object} tree A global styles object. + * + * @return {Object} Style object containing with + * + */ +export function getMergedVariation( variation, tree ) { + const referencedVariation = variation?.ref + ? getValueFromObjectPath( tree, variation.ref ) + : {}; + const mergedVariation = JSON.parse( JSON.stringify( referencedVariation ) ); + deepMerge( mergedVariation, variation ); + return mergedVariation; +} From bc1c5bb41f0e85013e73b9aafc6e1654ca35cbc2 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:41:47 +1000 Subject: [PATCH 04/13] Include referenced variation styles in UI --- .../components/global-styles/screen-block.js | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index def7d1d627fe46..08b440a0c62bdd 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -78,6 +78,27 @@ const { AdvancedPanel: StylesAdvancedPanel, } = unlock( blockEditorPrivateApis ); +// This code is duplicated from utils within the block editor package. +// Should the block editor export these utils for reuse? +const isObject = ( item ) => + !! item && typeof item === 'object' && ! Array.isArray( item ); + +const deepMerge = ( target, source ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + const getter = Object.getOwnPropertyDescriptor( source, key )?.get; + if ( typeof getter === 'function' ) { + Object.defineProperty( target, key, { get: getter } ); + } else if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + function ScreenBlock( { name, variation } ) { let prefixParts = []; if ( variation ) { @@ -88,9 +109,23 @@ function ScreenBlock( { name, variation } ) { const [ style ] = useGlobalStyle( prefix, name, 'user', { shouldDecodeEncode: false, } ); - const [ inheritedStyle, setStyle ] = useGlobalStyle( prefix, name, 'all', { - shouldDecodeEncode: false, - } ); + const [ rawInheritedStyle, setStyle ] = useGlobalStyle( + prefix, + name, + 'all', + { + shouldDecodeEncode: false, + } + ); + const [ variationStyles ] = useGlobalStyle( + `blocks.variations.${ variation }` + ); + + const inheritedStyle = variationStyles + ? JSON.parse( JSON.stringify( variationStyles ) ) + : {}; + deepMerge( inheritedStyle, rawInheritedStyle ); + const [ userSettings ] = useGlobalSetting( '', name, 'user' ); const [ rawSettings, setSettings ] = useGlobalSetting( '', name ); const settings = useSettingsForBlockElement( rawSettings, name ); From 7753561908b382ce09b72b40cab423d8d155d35a Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:11:24 +1000 Subject: [PATCH 05/13] Revert "Include referenced variation styles in UI" This reverts commit bc1c5bb41f0e85013e73b9aafc6e1654ca35cbc2. --- .../components/global-styles/screen-block.js | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index 08b440a0c62bdd..def7d1d627fe46 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -78,27 +78,6 @@ const { AdvancedPanel: StylesAdvancedPanel, } = unlock( blockEditorPrivateApis ); -// This code is duplicated from utils within the block editor package. -// Should the block editor export these utils for reuse? -const isObject = ( item ) => - !! item && typeof item === 'object' && ! Array.isArray( item ); - -const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - const getter = Object.getOwnPropertyDescriptor( source, key )?.get; - if ( typeof getter === 'function' ) { - Object.defineProperty( target, key, { get: getter } ); - } else if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - function ScreenBlock( { name, variation } ) { let prefixParts = []; if ( variation ) { @@ -109,23 +88,9 @@ function ScreenBlock( { name, variation } ) { const [ style ] = useGlobalStyle( prefix, name, 'user', { shouldDecodeEncode: false, } ); - const [ rawInheritedStyle, setStyle ] = useGlobalStyle( - prefix, - name, - 'all', - { - shouldDecodeEncode: false, - } - ); - const [ variationStyles ] = useGlobalStyle( - `blocks.variations.${ variation }` - ); - - const inheritedStyle = variationStyles - ? JSON.parse( JSON.stringify( variationStyles ) ) - : {}; - deepMerge( inheritedStyle, rawInheritedStyle ); - + const [ inheritedStyle, setStyle ] = useGlobalStyle( prefix, name, 'all', { + shouldDecodeEncode: false, + } ); const [ userSettings ] = useGlobalSetting( '', name, 'user' ); const [ rawSettings, setSettings ] = useGlobalSetting( '', name ); const settings = useSettingsForBlockElement( rawSettings, name ); From f8897fedde9a896b10c6fc2c1a5764c1091f01de Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:11:27 +1000 Subject: [PATCH 06/13] Revert "Output referenced variation styles in site editor Globals Styles" This reverts commit 278801f230837fa0684d3b8842148c4b16744f48. --- .../global-styles/use-global-styles-output.js | 8 +--- .../src/components/global-styles/utils.js | 42 ------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 51655636f2d014..b1f6f395d9861f 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -21,7 +21,6 @@ import { ROOT_BLOCK_SELECTOR, scopeSelector, appendToSelector, - getMergedVariation, } from './utils'; import { getBlockCSSSelector } from './get-block-css-selector'; import { @@ -673,14 +672,9 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { const variations = {}; Object.entries( node.variations ).forEach( - ( [ variationName, variationNode ] ) => { - const variation = getMergedVariation( - variationNode, - tree - ); + ( [ variationName, variation ] ) => { variations[ variationName ] = pickStyleKeys( variation ); - const variationSelector = blockSelectors[ blockName ].styleVariationSelectors[ variationName diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index e276245ddcf6dc..08e38592cfa865 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -450,45 +450,3 @@ export function areGlobalStyleConfigsEqual( original, variation ) { fastDeepEqual( original?.settings, variation?.settings ) ); } - -// TODO: Is this the right place for these utils? -// They're only used in the useGlobalStylesOutput hook and the block screen in -// the site editor. The block editor's utils/object.js is exported. Do we want -// that? -export const isObject = ( item ) => - !! item && typeof item === 'object' && ! Array.isArray( item ); - -export const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - const getter = Object.getOwnPropertyDescriptor( source, key )?.get; - if ( typeof getter === 'function' ) { - Object.defineProperty( target, key, { get: getter } ); - } else if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - -/** - * Retrieves any referenced block style variation data and overrides that with - * the current variation style object values. - * - * @param {Object} variation Style object for block style variation. - * @param {Object} tree A global styles object. - * - * @return {Object} Style object containing with - * - */ -export function getMergedVariation( variation, tree ) { - const referencedVariation = variation?.ref - ? getValueFromObjectPath( tree, variation.ref ) - : {}; - const mergedVariation = JSON.parse( JSON.stringify( referencedVariation ) ); - deepMerge( mergedVariation, variation ); - return mergedVariation; -} From 30a5a276034e213e6a5cd20116122251523b5280 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:11:39 +1000 Subject: [PATCH 07/13] Revert "Process theme.json referenced block style variations" This reverts commit 8b985b7e64a1090802a120bf927fb4c412aa171a. --- lib/class-wp-theme-json-gutenberg.php | 106 +------------------------- 1 file changed, 2 insertions(+), 104 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index e54bdc3023e52d..58d29c14e7c025 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -862,26 +862,6 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $block_style_variation_styles['blocks'] = $schema_styles_blocks; $block_style_variation_styles['elements'] = $schema_styles_elements; - // Generate schema for shared variations i.e. those that can be - // referenced with block style variations and live under - // styles.blocks.variations. These variations could be any valid block - // style variation. The schema will differ in that these variations - // cannot reference another. - // - // NOTE: The size of the schema array is already very large given - // entries for each individual block. This is compounded when multiple - // variations need to added to the schema. Would multiple passes for - // validation offer any improvements? - $unique_variations = array_unique( - call_user_func_array( 'array_merge', array_values( $valid_variations ) ) - ); - $schema_shared_style_variations = array_fill_keys( $unique_variations, $block_style_variation_styles ); - - // Allow refs only within the individual block type variations properties. - // Assigning it before `$schema_shared_style_variations` would mean - // shared variations would allow `ref` properties. - $block_style_variation_styles['ref'] = null; - foreach ( $valid_block_names as $block ) { $style_variation_names = array(); @@ -910,7 +890,6 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema['styles'] = static::VALID_STYLES; $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['blocks']['variations'] = $schema_shared_style_variations; $schema['styles']['elements'] = $schema_styles_elements; $schema['settings'] = static::VALID_SETTINGS; $schema['settings']['blocks'] = $schema_settings_blocks; @@ -2472,15 +2451,6 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { 'selector' => $variation_selector, ); - if ( - isset( $variation_node['ref'] ) && - str_starts_with( $variation_node['ref'], 'styles.blocks.variations' ) - ) { - $variation_path = explode( '.', $variation_node['ref'] ); - $referenced_variation = _wp_array_get( $theme_json, $variation_path, array() ); - $variation_node = static::merge_styles( $referenced_variation, $variation_node ); - } - $variation_blocks = $variation_node['blocks'] ?? array(); $variation_elements = $variation_node['elements'] ?? array(); @@ -2568,30 +2538,6 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { return $nodes; } - /** - * Retrieves a style node along with any of its non-customized block style - * variation data. - * - * @param array $theme_json A theme.json structure to modify. - * @param array $path Path for the node to retrie. - * - * @return array Style node with merged block style variation data if appropriate. - */ - public static function get_style_node_with_referenced_variations( $theme_json, $path ) { - $node = _wp_array_get( $theme_json, $path, array() ); - $index = array_search( 'variations', $path, true ); - - // Get any referenced variation and merge any node values into that. - if ( false !== $index && isset( $path[ $index + 1 ] ) ) { - $variation_path = array( 'styles', 'blocks', 'variations' ); - $variation_path = array_merge( $variation_path, array_slice( $path, $index + 1 ) ); - $variation_node = _wp_array_get( $theme_json, $variation_path, array() ); - $node = static::merge_styles( $variation_node, $node ); - } - - return $node; - } - /** * Gets the CSS rules for a particular block from theme.json. * @@ -2602,7 +2548,7 @@ public static function get_style_node_with_referenced_variations( $theme_json, $ * @return string Styles for the block. */ public function get_styles_for_block( $block_metadata ) { - $node = static::get_style_node_with_referenced_variations( $this->theme_json, $block_metadata['path'] ); + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; $selector = $block_metadata['selector']; $settings = $this->theme_json['settings'] ?? null; @@ -2613,21 +2559,7 @@ public function get_styles_for_block( $block_metadata ) { $style_variation_declarations = array(); if ( ! empty( $block_metadata['variations'] ) ) { foreach ( $block_metadata['variations'] as $style_variation ) { - // Given block style variations can reference an entire object - // rather than a single value, any values in the variation - // node itself should override the referenced style variation's - // values. - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); - if ( - isset( $style_variation_node['ref'] ) && - str_starts_with( $style_variation_node['ref'], 'styles.blocks.variations' ) - ) { - $variation_path = explode( '.', $style_variation_node['ref'] ); - $referenced_variation = _wp_array_get( $this->theme_json, $variation_path, array() ); - $style_variation_node = static::merge_styles( $referenced_variation, $style_variation_node ); - } - - $style_variation_node = $style_variation_node ? $style_variation_node : array(); + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); $clean_style_variation_selector = trim( $style_variation['selector'] ); // Generate any feature/subfeature style declarations for the current style variation. @@ -4111,38 +4043,4 @@ function ( $carry, $item ) { $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); return $theme_json; } - - /** - * Recursively merge style objects without merging leaf values. - * - * `array_merge_recursive` will end up merging style values if they are - * present in both style objects resulting in an array when the value - * should be a string. - * - * @param array $target_styles Target style data. - * @param array $source_styles Style data to merge into the target. - * - * @return array Merged style data - */ - public static function merge_styles( $target_styles, $source_styles ) { - if ( empty( $source_styles ) ) { - return $target_styles; - } - - if ( empty( $target_styles ) ) { - return $source_styles; - } - - $merged_styles = $target_styles; - - foreach ( $source_styles as $key => $value ) { - if ( is_array( $value ) && isset( $merged_styles[ $key ] ) && is_array( $merged_styles[ $key ] ) ) { - $merged_styles[ $key ] = static::merge_styles( $merged_styles[ $key ], $value ); - } else { - $merged_styles[ $key ] = $value; - } - } - - return $merged_styles; - } } From 51bb763ec8b2c08191c5e6e3d29d6394e439aa79 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:11:45 +1000 Subject: [PATCH 08/13] Revert "Add styles.blocks.variations to theme.json schema" This reverts commit 3153e0e11d54cb8d2d1231ef90566da276c9073c. --- schemas/json/theme.json | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/schemas/json/theme.json b/schemas/json/theme.json index cb709ccb672923..30016a16c38f9d 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -2164,9 +2164,6 @@ }, "core/widget-group": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" - }, - "variations": { - "$ref": "#/definitions/stylesVariationsPropertiesComplete" } }, "patternProperties": { @@ -2197,7 +2194,7 @@ "$ref": "#/definitions/stylesElementsPropertiesComplete" }, "variations": { - "$ref": "#/definitions/stylesVariationsPropertiesAndRefComplete" + "$ref": "#/definitions/stylesVariationsPropertiesComplete" } }, "additionalProperties": false @@ -2212,21 +2209,6 @@ } } }, - "stylesVariationsPropertiesAndRefComplete": { - "type": "object", - "patternProperties": { - "^[a-z][a-z0-9-]*$": { - "oneOf": [ - { - "$ref": "#/definitions/refComplete" - }, - { - "$ref": "#/definitions/stylesVariationPropertiesComplete" - } - ] - } - } - }, "stylesVariationPropertiesComplete": { "type": "object", "allOf": [ From b10f7ad08034ad71fcd11946dbbbce8ec86d7bc5 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:16:36 +1000 Subject: [PATCH 09/13] Add supportedBlockTypes property to theme.json schema --- schemas/json/theme.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 30016a16c38f9d..2b0c6ac33536b2 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -2584,6 +2584,13 @@ "type": "string", "description": "Description of the global styles variation." }, + "supportedBlockTypes": { + "type": "array", + "description": "List of block types that can use the block style variation this theme.json file represents.", + "items": { + "type": "string" + } + }, "settings": { "description": "Settings for the block editor and individual blocks. These include things like:\n- Which customization options should be available to the user. \n- The default colors, font sizes... available to the user. \n- CSS custom properties and class names used in styles.\n- And the default layout of the editor (widths and available alignments).", "type": "object", From 29125ef428a172ae3e84887f1e946cd0a504c9e0 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:08:41 +1000 Subject: [PATCH 10/13] Add supportedBlockTypes to valid top level properties in theme.json --- lib/class-wp-theme-json-gutenberg.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 58d29c14e7c025..1cec3e467ce36e 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -328,6 +328,7 @@ class WP_Theme_JSON_Gutenberg { 'patterns', 'settings', 'styles', + 'supportedBlockTypes', 'templateParts', 'title', 'version', From c66f888cc458640526d1aa6757160251fea47ba4 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:10:15 +1000 Subject: [PATCH 11/13] Absorb shared block style variations into theme.json from stand alone files --- ...class-wp-theme-json-resolver-gutenberg.php | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 0ad947957215fc..11448a45d6fa57 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -225,7 +225,8 @@ protected static function has_same_registered_blocks( $origin ) { * @since 5.8.0 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed. * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports. - * @since 6.5.0 Theme data will now also include block style variations that were registered with a style object. + * @since 6.5.0 Theme data will now also include block style variations that + * were registered with a style object or included via a standalone file. * * @param array $deprecated Deprecated. Not used. * @param array $options { @@ -373,9 +374,53 @@ public static function get_theme_data( $deprecated = array(), $options = array() $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); if ( $options['with_block_style_variations'] ) { + // Absorb block style variations that were registered with a style object. $block_style_variations_data = WP_Theme_JSON_Gutenberg::get_from_block_styles_registry(); $with_block_style_variations = new WP_Theme_JSON_Gutenberg( $block_style_variations_data ); $with_theme_supports->merge( $with_block_style_variations ); + + // Resolve shared block style variations that were bundled in the + // theme via standalone theme.json files. + $shared_block_style_variations = static::get_style_variations( '/block-styles' ); + $variations_data = array(); + $registry = WP_Block_Styles_Registry::get_instance(); + + foreach ( $shared_block_style_variations as $variation ) { + if ( empty( $variation['supportedBlockTypes'] ) || empty( $variation['styles'] ) ) { + continue; + } + + $variation_slug = _wp_to_kebab_case( $variation['title'] ); + + // If it proves desirable, block style variations could include + // custom settings which can be included here. + foreach ( $variation['supportedBlockTypes'] as $block_type ) { + // Automatically register the block style variation if it + // hasn't been already. + $registered_styles = $registry->get_registered_styles_for_block( $block_type ); + if ( ! array_key_exists( $variation_slug, $registered_styles ) ) { + gutenberg_register_block_style( + $block_type, + array( + 'name' => $variation_slug, + 'label' => $variation['title'], + ) + ); + } + + $path = array( $block_type, 'variations', $variation_slug ); + _wp_array_set( $variations_data, $path, $variation['styles'] ); + } + } + + if ( ! empty( $variations_data ) ) { + $variations_theme_json_data = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( 'blocks' => $variations_data ), + ); + $with_shared_variations = new WP_Theme_JSON_Gutenberg( $variations_theme_json_data ); + $with_theme_supports->merge( $with_shared_variations ); + } } $with_theme_supports->merge( static::$theme ); @@ -743,14 +788,16 @@ private static function recursively_iterate_json( $dir ) { * Returns the style variations defined by the theme (parent and child). * * @since 6.2.0 Returns parent theme variations if theme is a child. + * @since 6.5.0 Added configurable directory to allow block style variations + * to reside in a different directory to theme style variations. * * @return array */ - public static function get_style_variations() { + public static function get_style_variations( $dir = 'styles' ) { $variation_files = array(); $variations = array(); - $base_directory = get_stylesheet_directory() . '/styles'; - $template_directory = get_template_directory() . '/styles'; + $base_directory = get_stylesheet_directory() . '/' . $dir; + $template_directory = get_template_directory() . '/' . $dir; if ( is_dir( $base_directory ) ) { $variation_files = static::recursively_iterate_json( $base_directory ); } From 88c7e68c8b745ae324783efc65e5a126f3831d42 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:25:33 +1000 Subject: [PATCH 12/13] Update test themes for block style variations --- .../block-styles/block-style-variation-a.json | 10 ++++++++++ .../style.css | 8 ++++++++ .../theme.json | 4 ++++ .../block-styles/block-style-variation-a.json | 10 ++++++++++ .../block-styles/block-style-variation-b.json | 10 ++++++++++ 5 files changed, 42 insertions(+) create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/block-styles/block-style-variation-a.json create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json create mode 100644 phpunit/data/themedir1/block-theme/block-styles/block-style-variation-a.json create mode 100644 phpunit/data/themedir1/block-theme/block-styles/block-style-variation-b.json diff --git a/phpunit/data/themedir1/block-theme-child-with-block-style-variations/block-styles/block-style-variation-a.json b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/block-styles/block-style-variation-a.json new file mode 100644 index 00000000000000..1daaac0062b9c6 --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/block-styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "supportedBlockTypes": [ "core/group", "core/columns", "core/media-text" ], + "styles": { + "color": { + "background": "darkcyan", + "text": "aliceblue" + } + } +} diff --git a/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css new file mode 100644 index 00000000000000..c1cc20aaf1f101 --- /dev/null +++ b/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/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json new file mode 100644 index 00000000000000..0da29ef16fd679 --- /dev/null +++ b/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": 2 +} diff --git a/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-a.json b/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-a.json new file mode 100644 index 00000000000000..0ba8417049eb0a --- /dev/null +++ b/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "supportedBlockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "indigo", + "text": "plum" + } + } +} diff --git a/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-b.json b/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-b.json new file mode 100644 index 00000000000000..6133b3e9f8d591 --- /dev/null +++ b/phpunit/data/themedir1/block-theme/block-styles/block-style-variation-b.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "supportedBlockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "midnightblue", + "text": "lightblue" + } + } +} From d7cdf3f634d8f659252c031476f6dcb34522518e Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:25:51 +1000 Subject: [PATCH 13/13] Update unit tests for shared block style variations --- phpunit/class-wp-theme-json-resolver-test.php | 159 +++++++++++++----- 1 file changed, 114 insertions(+), 45 deletions(-) diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index aff82bc145b816..e820c292708da5 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -226,6 +226,12 @@ public function test_add_theme_supports_are_loaded_for_themes_without_theme_json $this->assertSame( $color_palette, $settings['color']['palette']['theme'] ); } + /** + * Tests that block style variations registered via either + * `gutenberg_register_block_style` with a style object, or a standalone + * block style variation file within `/block-styles`, are added to the + * theme data. + */ public function test_add_registered_block_styles_to_theme_data() { switch_theme( 'block-theme' ); @@ -267,7 +273,22 @@ public function test_add_registered_block_styles_to_theme_data() { $group_styles = $theme_json['styles']['blocks']['core/group'] ?? array(); $expected = array( 'variations' => array( - 'my-variation' => $variation_styles_data, + 'my-variation' => $variation_styles_data, + // The following variations are registered automatically from + // their respective JSON files within the theme's `block-styles` + // directory. + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'indigo', + 'text' => 'plum', + ), + ), + 'block-style-variation-b' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', + ), + ), ), ); @@ -580,54 +601,81 @@ public function data_get_merged_data_returns_origin() { ); } - /** - * Test 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 style variations (`/styles`) and block style + * variations (`/block-styles`). * * @covers WP_Theme_JSON_Resolver::get_style_variations - **/ - public function test_get_style_variations_returns_all_variations() { - // Switch to a child theme. - switch_theme( 'block-theme-child' ); + * + * @dataProvider data_get_style_variations + * + * @param string $theme Name of the theme to use. + * @param string $dir The directory to retrieve variation json files from. + * @param array $expected_variations Collection of expected variations. + */ + public function test_get_style_variations( $theme, $dir, $expected_variations ) { + switch_theme( $theme ); wp_set_current_user( self::$administrator_id ); - $actual_settings = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations(); - $expected_settings = array( - array( - 'version' => 2, - '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_Gutenberg::get_style_variations( $dir ); + + self::recursive_ksort( $actual_variations ); + self::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( + 'theme_style_variations' => array( + 'theme' => 'block-theme-child', + 'dir' => 'styles', + 'expected_variations' => array( + array( + 'version' => 2, + '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' => 2, - 'title' => 'variation-b', - 'settings' => array( - 'blocks' => array( - 'core/post-title' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'light', - 'name' => 'Light', - 'color' => '#f1f1f1', + array( + 'version' => 2, + 'title' => 'variation-b', + 'settings' => array( + 'blocks' => array( + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f1f1f1', + ), + ), ), ), ), @@ -636,13 +684,34 @@ public function test_get_style_variations_returns_all_variations() { ), ), ), - ); - self::recursive_ksort( $actual_settings ); - self::recursive_ksort( $expected_settings ); - - $this->assertSame( - $expected_settings, - $actual_settings + 'block_style_variations' => array( + 'theme' => 'block-theme-child-with-block-style-variations', + 'dir' => 'block-styles', + 'expected_variations' => array( + array( + 'supportedBlockTypes' => array( 'core/group', 'core/columns', 'core/media-text' ), + 'version' => 2, + 'title' => 'block-style-variation-a', + 'styles' => array( + 'color' => array( + 'background' => 'darkcyan', + 'text' => 'aliceblue', + ), + ), + ), + array( + 'supportedBlockTypes' => array( 'core/group', 'core/columns' ), + 'version' => 2, + 'title' => 'block-style-variation-b', + 'styles' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', + ), + ), + ), + ), + ), ); } }