From 97a0b1e7f3aae691f219d0a793d69e30db4fec2f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 6 Feb 2023 15:31:50 +0000 Subject: [PATCH] Block editor: Update WP_Theme_JSON_Resolver and improve its performance. This commit includes the latest updates WP_Theme_JSON_Resolver class made in the block editor. Some of these updates improve the performance of the class. Props Mamaduka, hellofromTonya, flixos90, jorgefilipecosta, oandregal, spacedmonkey, audrasjb, costdev, scruffian. Closes #57545. git-svn-id: https://develop.svn.wordpress.org/trunk@55231 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-theme-json-resolver.php | 114 +++++++--- .../block-theme-child/styles/variation-b.json | 18 ++ .../block-theme/styles/variation-b.json | 18 ++ .../data/themedir1/block-theme/theme.json | 2 +- .../rest-global-styles-controller.php | 27 ++- .../tests/theme/wpThemeJsonResolver.php | 207 +++++++++++++++++- 6 files changed, 349 insertions(+), 37 deletions(-) create mode 100644 tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json create mode 100644 tests/phpunit/data/themedir1/block-theme/styles/variation-b.json diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 7b463681d6f12..d178f076e9251 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -427,14 +427,16 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post $post_type_filter = 'wp_global_styles'; $stylesheet = $theme->get_stylesheet(); $args = array( - 'posts_per_page' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - 'tax_query' => array( + 'posts_per_page' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'post_type' => $post_type_filter, + 'post_status' => $post_status_filter, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'tax_query' => array( array( 'taxonomy' => 'wp_theme', 'field' => 'name', @@ -446,7 +448,7 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post $global_style_query = new WP_Query(); $recent_posts = $global_style_query->query( $args ); if ( count( $recent_posts ) === 1 ) { - $user_cpt = get_post( $recent_posts[0], ARRAY_A ); + $user_cpt = get_object_vars( $recent_posts[0] ); } elseif ( $create_post ) { $cpt_post_id = wp_insert_post( array( @@ -462,7 +464,7 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post true ); if ( ! is_wp_error( $cpt_post_id ) ) { - $user_cpt = get_post( $cpt_post_id, ARRAY_A ); + $user_cpt = get_object_vars( get_post( $cpt_post_id ) ); } } @@ -525,9 +527,15 @@ public static function get_user_data() { /** * Returns the data merged from multiple origins. * - * There are three sources of data (origins) for a site: - * default, theme, and custom. The custom's has higher priority - * than the theme's, and the theme's higher than default's. + * There are four sources of data (origins) for a site: + * + * - default => WordPress + * - blocks => each one of the blocks provides data for itself + * - theme => the active theme + * - custom => data provided by the user + * + * The custom's has higher priority than the theme's, the theme's higher than blocks', + * and block's higher than default's. * * Unlike the getters * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data}, @@ -535,7 +543,7 @@ public static function get_user_data() { * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data}, * this method returns data after it has been merged with the previous origins. * This means that if the same piece of data is declared in different origins - * (user, theme, and core), the last origin overrides the previous. + * (default, blocks, theme, custom), the last origin overrides the previous. * * For example, if the user has set a background color * for the paragraph block, and the theme has done it as well, @@ -545,9 +553,10 @@ public static function get_user_data() { * @since 5.9.0 Added user data, removed the `$settings` parameter, * added the `$origin` parameter. * @since 6.1.0 Added block data and generation of spacingSizes array. + * @since 6.2.0 Changed ' $origin' parameter values to 'default', 'blocks', 'theme' or 'custom'. * - * @param string $origin Optional. To what level should we merge data. - * Valid values are 'theme' or 'custom'. Default 'custom'. + * @param string $origin Optional. To what level should we merge data: 'default', 'blocks', 'theme' or 'custom'. + * 'custom' is used as default value as well as fallback value if the origin is unknown. * @return WP_Theme_JSON */ public static function get_merged_data( $origin = 'custom' ) { @@ -556,14 +565,23 @@ public static function get_merged_data( $origin = 'custom' ) { } $result = static::get_core_data(); + if ( 'default' === $origin ) { + $result->set_spacing_sizes(); + return $result; + } + $result->merge( static::get_block_data() ); - $result->merge( static::get_theme_data() ); + if ( 'blocks' === $origin ) { + return $result; + } - if ( 'custom' === $origin ) { - $result->merge( static::get_user_data() ); + $result->merge( static::get_theme_data() ); + if ( 'theme' === $origin ) { + $result->set_spacing_sizes(); + return $result; } - // Generate the default spacingSizes array based on the merged spacingScale settings. + $result->merge( static::get_user_data() ); $result->set_spacing_sizes(); return $result; @@ -649,33 +667,61 @@ public static function clean_cached_data() { static::$i18n_schema = null; } + /** + * Returns an array of all nested JSON files within a given directory. + * + * @since 6.2.0 + * + * @param string $dir The directory to recursively iterate and list files of. + * @return array The merged array. + */ + private static function recursively_iterate_json( $dir ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) ); + $nested_json_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) ); + return $nested_json_files; + } + + /** * 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. * * @return array */ public static function get_style_variations() { - $variations = array(); - $base_directory = get_stylesheet_directory() . '/styles'; + $variation_files = array(); + $variations = array(); + $base_directory = get_stylesheet_directory() . '/styles'; + $template_directory = get_template_directory() . '/styles'; if ( is_dir( $base_directory ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) ); - $nested_html_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) ); - ksort( $nested_html_files ); - foreach ( $nested_html_files as $path => $file ) { - $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { - $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); - $variation = ( new WP_Theme_JSON( $translated ) )->get_raw_data(); - if ( empty( $variation['title'] ) ) { - $variation['title'] = basename( $path, '.json' ); + $variation_files = static::recursively_iterate_json( $base_directory ); + } + if ( is_dir( $template_directory ) && $template_directory !== $base_directory ) { + $variation_files_parent = static::recursively_iterate_json( $template_directory ); + // If the child and parent variation file basename are the same, only include the child theme's. + foreach ( $variation_files_parent as $parent_path => $parent ) { + foreach ( $variation_files as $child_path => $child ) { + if ( basename( $parent_path ) === basename( $child_path ) ) { + unset( $variation_files_parent[ $parent_path ] ); } - $variations[] = $variation; } } + $variation_files = array_merge( $variation_files, $variation_files_parent ); + } + ksort( $variation_files ); + foreach ( $variation_files as $path => $file ) { + $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) ); + if ( is_array( $decoded_file ) ) { + $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); + $variation = ( new WP_Theme_JSON( $translated ) )->get_raw_data(); + if ( empty( $variation['title'] ) ) { + $variation['title'] = basename( $path, '.json' ); + } + $variations[] = $variation; + } } return $variations; } - } diff --git a/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json new file mode 100644 index 0000000000000..0a8a4fcab99f6 --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json @@ -0,0 +1,18 @@ +{ + "version": 2, + "settings": { + "blocks": { + "core/post-title": { + "color": { + "palette": [ + { + "slug": "dark", + "name": "Dark", + "color": "#010101" + } + ] + } + } + } + } +} diff --git a/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json b/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json new file mode 100644 index 0000000000000..340198ffe0b65 --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json @@ -0,0 +1,18 @@ +{ + "version": 2, + "settings": { + "blocks": { + "core/post-title": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f1f1f1" + } + ] + } + } + } + } +} diff --git a/tests/phpunit/data/themedir1/block-theme/theme.json b/tests/phpunit/data/themedir1/block-theme/theme.json index d023faec53125..76d075aab7d28 100644 --- a/tests/phpunit/data/themedir1/block-theme/theme.json +++ b/tests/phpunit/data/themedir1/block-theme/theme.json @@ -124,4 +124,4 @@ "area": "header" } ] -} \ No newline at end of file +} diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index f20fde92ae663..e71a05e12d912 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -484,6 +484,27 @@ public function test_get_theme_items() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $expected = array( + 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' => 'Block theme variation', @@ -511,7 +532,11 @@ public function test_get_theme_items() { ), ), ); - $this->assertSameSetsWithIndex( $data, $expected ); + + wp_recursive_ksort( $data ); + wp_recursive_ksort( $expected ); + + $this->assertSameSets( $data, $expected ); } /** diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index a835b0a3ffb9f..0676e01100d41 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -235,7 +235,7 @@ public function test_translations_are_applied() { ); $this->assertSame( 'Wariant motywu blokowego', - $style_variations[0]['title'] + $style_variations[1]['title'] ); } @@ -775,4 +775,209 @@ public function test_get_theme_data_does_not_parse_theme_json_if_not_present() { $this->assertSame( $empty_theme_json, $theme_data->get_raw_data(), 'Theme data should be empty without theme support.' ); $this->assertNull( $property->getValue(), 'Theme i18n schema should not have been loaded without theme support.' ); } + + /** + * Tests that get_merged_data returns the data merged up to the proper origin. + * + * @ticket 57545 + * + * @covers WP_Theme_JSON_Resolver::get_merged_data + * + * @dataProvider data_get_merged_data_returns_origin + * + * @param string $origin What origin to get data from. + * @param bool $core_palette Whether the core palette is present. + * @param string $core_palette_text Message. + * @param string $block_styles Whether the block styles are present. + * @param string $block_styles_text Message. + * @param bool $theme_palette Whether the theme palette is present. + * @param string $theme_palette_text Message. + * @param bool $user_palette Whether the user palette is present. + * @param string $user_palette_text Message. + */ + public function test_get_merged_data_returns_origin( $origin, $core_palette, $core_palette_text, $block_styles, $block_styles_text, $theme_palette, $theme_palette_text, $user_palette, $user_palette_text ) { + // Make sure there is data from the blocks origin. + register_block_type( + 'my/block-with-styles', + array( + 'api_version' => 2, + 'attributes' => array( + 'borderColor' => array( + 'type' => 'string', + ), + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + '__experimentalStyle' => array( + 'typography' => array( + 'fontSize' => '42rem', + ), + ), + ), + ) + ); + + // Make sure there is data from the theme origin. + switch_theme( 'block-theme' ); + + // Make sure there is data from the user origin. + wp_set_current_user( self::$administrator_id ); + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + $config = json_decode( $user_cpt['post_content'], true ); + $config['settings']['color']['palette']['custom'] = array( + array( + 'color' => 'hotpink', + 'name' => 'My color', + 'slug' => 'my-color', + ), + ); + $user_cpt['post_content'] = wp_json_encode( $config ); + wp_update_post( $user_cpt, true, false ); + + $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $origin ); + $settings = $theme_json->get_settings(); + $styles = $theme_json->get_styles_block_nodes(); + $styles = array_filter( + $styles, + static function( $element ) { + return isset( $element['name'] ) && 'my/block-with-styles' === $element['name']; + } + ); + unregister_block_type( 'my/block-with-styles' ); + + $this->assertSame( $core_palette, isset( $settings['color']['palette']['default'] ), $core_palette_text ); + $this->assertSame( $block_styles, count( $styles ) === 1, $block_styles_text ); + $this->assertSame( $theme_palette, isset( $settings['color']['palette']['theme'] ), $theme_palette_text ); + $this->assertSame( $user_palette, isset( $settings['color']['palette']['custom'] ), $user_palette_text ); + + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_get_merged_data_returns_origin() { + return array( + 'origin_default' => array( + 'origin' => 'default', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => false, + 'block_styles_text' => 'Block styles should not be present', + 'theme_palette' => false, + 'theme_palette_text' => 'Theme palette should not be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_blocks' => array( + 'origin' => 'blocks', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => false, + 'theme_palette_text' => 'Theme palette should not be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_theme' => array( + 'origin' => 'theme', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => true, + 'theme_palette_text' => 'Theme palette must be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_custom' => array( + 'origin' => 'custom', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => true, + 'theme_palette_text' => 'Theme palette must be present', + 'user_palette' => true, + 'user_palette_text' => 'User palette must be present', + ), + ); + } + + /** + * 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. + * + * @ticket 57545 + * + * @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' ); + wp_set_current_user( self::$administrator_id ); + + $actual_settings = WP_Theme_JSON_Resolver::get_style_variations(); + $expected_settings = array( + array( + 'version' => 2, + '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' => 2, + 'title' => 'Block theme variation', + 'settings' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'foreground', + 'name' => 'Foreground', + 'color' => '#3F67C6', + ), + ), + ), + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/post-title' => array( + 'typography' => array( + 'fontWeight' => '700', + ), + ), + ), + ), + ), + ); + + wp_recursive_ksort( $actual_settings ); + wp_recursive_ksort( $expected_settings ); + + $this->assertSame( + $expected_settings, + $actual_settings + ); + } }