From 6a810170ee3a9c1a7bda491b1c89b487623ec3f8 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 26 Jul 2022 11:00:26 -0400 Subject: [PATCH 1/4] Implement support for merging theme.json provided by plugins. --- ...class-wp-theme-json-resolver-gutenberg.php | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php index adf5803de561aa..183b6ee39df121 100644 --- a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php @@ -16,6 +16,15 @@ * @access private */ class WP_Theme_JSON_Resolver_Gutenberg extends WP_Theme_JSON_Resolver_6_1 { + + /** + * Container for data coming from plugins + * + * @since 6.1.0 + * @var WP_Theme_JSON + */ + protected static $plugins = null; + /** * Returns the theme's data. * @@ -127,6 +136,44 @@ public static function get_block_data() { return new WP_Theme_JSON_Gutenberg( $config, 'core' ); } + /** + * Returns any plugin provided theme data from all plugins merged together. + * + * Note: The current merge strategy merges in order of plugins returned by + * `wp_get_active_and_valid_plugins()`. This means the data from the last + * plugin in the list will override any equivalent properties from those + * earlier in the list. + * + * @return WP_Theme_JSON + */ + public static function get_plugin_theme_data() { + if ( null === static::$plugins ) { + $plugins_data = array(); + $active_plugin_paths = wp_get_active_and_valid_plugins(); + foreach ( $active_plugin_paths as $path ) { + $config = static::read_json_file( dirname( $path ) . '/theme.json' ); + if ( ! empty( $config ) ) { + $plugins_data[ $path ] = $config; + } + } + // have configs from plugins, now let's register and merge. + $plugin_json = new WP_Theme_JSON_Gutenberg(); + foreach ( $plugins_data as $plugin_path => $plugin_config ) { + $plugin_meta_data = get_plugin_data( $plugin_path, false, false ); + if ( isset( $plugin_meta_data['TextDomain'] ) ) { + $plugin_config = static::translate( $plugin_config, $plugin_meta_data['TextDomain'] ); + } + // TODO, this is where we could potentially introduce different merge + // strategies for plugin provided data. + $plugin_json->merge( + new WP_Theme_JSON_Gutenberg( $plugin_config ) + ); + } + static::$plugins = $plugin_json; + } + return static::$plugins; + } + /** * When given an array, this will remove any keys with the name `//`. * @@ -148,14 +195,15 @@ private static function remove_JSON_comments( $array ) { * 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. + * default, theme, plugin, and custom. The custom's has higher priority + * than the theme's, the theme's has higher priority than plugin's, + * and the plugin's has higher priority than the default's. * * Unlike the getters {@link get_core_data}, - * {@link get_theme_data}, and {@link get_user_data}, + * {@link get_theme_data}, {@link get_plugin_data}, and {@link 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), + * is declared in different origins (user, theme, plugin, and core), * the last origin overrides the previous. * * For example, if the user has set a background color @@ -178,7 +226,11 @@ public static function get_merged_data( $origin = 'custom' ) { $result = new WP_Theme_JSON_Gutenberg(); $result->merge( static::get_core_data() ); $result->merge( static::get_block_data() ); + // TODO: This is where we could potentially introduce new strategies for + // merging plugin provided data. + $result->merge( static::get_plugin_theme_data() ); $result->merge( static::get_theme_data() ); + if ( 'custom' === $origin ) { $result->merge( static::get_user_data() ); } @@ -187,4 +239,22 @@ public static function get_merged_data( $origin = 'custom' ) { return $result; } + + /** + * Cleans the cached data so it can be recalculated. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`, + * and `$i18n_schema` variables to reset. + * @since 6.1.0 Added the `$plugins` variables to reset. + */ + public static function clean_cached_data() { + static::$core = null; + static::$theme = null; + static::$user = null; + static::$user_custom_post_type_id = null; + static::$theme_has_support = null; + static::$i18n_schema = null; + static::$plugins = null; + } } From 10e627f99309ced3045c68c1461f1418df45181b Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 26 Jul 2022 11:02:31 -0400 Subject: [PATCH 2/4] Add function for cleaning cached data across instances of WP_Theme_JSON. This is needed for testing. --- lib/experimental/class-wp-theme-json-gutenberg.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/experimental/class-wp-theme-json-gutenberg.php b/lib/experimental/class-wp-theme-json-gutenberg.php index 3d6bf949e7f835..0f7cc8755e4576 100644 --- a/lib/experimental/class-wp-theme-json-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-gutenberg.php @@ -16,4 +16,13 @@ */ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_6_1 { + /** + * Cleans cached data across class instances. + * + * @since 6.1.0 + */ + public static function clean_cached_data() { + static::$blocks_metadata = null; + } + } From c9b55706407fdfd45a3be1c2847186ec83cb4834 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 26 Jul 2022 14:40:29 -0400 Subject: [PATCH 3/4] Add tests for new plugin theme.json merging functionality. --- .wp-env.json | 3 +- phpunit/class-wp-theme-json-resolver-test.php | 195 ++++++++++++++++++ .../test-plugin-theme-json.php | 10 + test/test-plugin-theme-json/theme.json | 42 ++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 test/test-plugin-theme-json/test-plugin-theme-json.php create mode 100644 test/test-plugin-theme-json/theme.json diff --git a/.wp-env.json b/.wp-env.json index aa8dfaf3c0c4ab..8e0b89c3db53e0 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -8,7 +8,8 @@ "wp-content/plugins/gutenberg": ".", "wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins", "wp-content/plugins/gutenberg-test-plugins": "./packages/e2e-tests/plugins", - "wp-content/themes/gutenberg-test-themes": "./test/gutenberg-test-themes" + "wp-content/themes/gutenberg-test-themes": "./test/gutenberg-test-themes", + "wp-content/plugins/test-plugin-theme-json": "./test/test-plugin-theme-json" } } } diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index e3704bfdc864f6..2365632d7d5122 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -293,4 +293,199 @@ function test_get_user_data_from_wp_global_styles_does_not_use_uncached_queries( $this->assertEquals( 0, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type' ); remove_filter( 'query', array( $this, 'filter_db_query' ) ); } + + function test_get_plugin_theme_json() { + WP_Theme_JSON_Gutenberg::clean_cached_data(); + activate_plugin( 'test-plugin-theme-json/test-plugin-theme-json.php' ); + register_block_type( 'mycustom/block', array( 'render_callback' => null ) ); + switch_theme( 'block-theme' ); + $actual = WP_Theme_JSON_Resolver_Gutenberg::get_plugin_theme_data()->get_raw_data(); + $expected = array( + 'settings' => array( + 'border' => array( + 'color' => true, + 'radius' => true, + 'style' => true, + 'width' => true, + ), + 'color' => array( + 'custom' => true, + 'customGradient' => true, + 'link' => true, + 'palette' => array( + 'theme' => array( + array( + 'color' => '#e44064', + 'name' => 'Pink Red', + 'slug' => 'pink-red', + ), + ), + ), + ), + 'spacing' => array( + 'blockGap' => true, + 'margin' => true, + 'padding' => true, + ), + 'typography' => array( + 'lineHeight' => true, + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'border' => array( + 'radius' => '10px', + ), + 'color' => array( + 'background' => 'var(--wp--preset--color--pink-red)', + ), + ), + 'mycustom/block' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--pink-red)', + ), + ), + ), + ), + 'version' => 2, + ); + self::recursive_ksort( $actual ); + self::recursive_ksort( $expected ); + + $this->assertSame( + $actual, + $expected + ); + unregister_block_type( 'mycustom/block' ); + deactivate_plugins( 'test-plugin-theme-json/test-plugin-theme-json.php' ); + } + + function test_merge_plugin_theme_json() { + WP_Theme_JSON_Gutenberg::clean_cached_data(); + activate_plugin( 'test-plugin-theme-json/test-plugin-theme-json.php' ); + register_block_type( 'mycustom/block', array( 'render_callback' => null ) ); + switch_theme( 'block-theme' ); + $raw_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_raw_data(); + $actual_settings = $raw_data['settings']; + $expected_settings = array( + 'border' => array( + 'color' => true, + 'radius' => true, + 'style' => true, + 'width' => true, + ), + 'color' => array( + 'custom' => false, + 'customGradient' => true, + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f3f4f6', + ), + array( + 'slug' => 'primary', + 'name' => 'Primary', + 'color' => '#3858e9', + ), + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#111827', + ), + array( + 'color' => '#e44064', + 'name' => 'Pink Red', + 'slug' => 'pink-red', + ), + ), + ), + 'link' => true, + ), + 'typography' => array( + 'customFontSize' => true, + 'lineHeight' => false, + ), + 'spacing' => array( + 'units' => false, + 'padding' => false, + ), + 'blocks' => array( + 'core/button' => array( + 'border' => array( + 'radius' => true, + ), + ), + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + ), + ), + ), + ), + 'core/pullquote' => array( + 'border' => array( + 'color' => true, + 'radius' => true, + 'style' => true, + 'width' => true, + ), + ), + ), + ); + + $raw_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_raw_data(); + $actual_styles = $raw_data['styles']; + $expected_styles = array( + 'blocks' => array( + 'core/button' => array( + 'border' => array( + 'radius' => '10px', + ), + 'color' => array( + 'background' => 'var(--wp--preset--color--pink-red)', + ), + ), + 'mycustom/block' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--pink-red)', + ), + ), + ), + ); + + self::recursive_ksort( $actual_settings ); + self::recursive_ksort( $expected_settings ); + self::recursive_ksort( $actual_styles ); + self::recursive_ksort( $expected_styles ); + + $this->assertSame( + $actual_settings['blocks'], + $expected_settings['blocks'], + 'The blocks index in the settings array does not match as expected' + ); + + $this->assertArrayHasKey( 'mycustom/block', $actual_styles['blocks'] ); + $this->assertArrayHasKey( 'core/button', $actual_styles['blocks'] ); + $this->assertSame( + $actual_styles['blocks']['mycustom/block'], + $expected_styles['blocks']['mycustom/block'], + 'The styles for mycustom/block should be the same' + ); + $this->assertSame( + $actual_styles['blocks']['core/button'], + $expected_styles['blocks']['core/button'], + 'The plugin theme.json should have its definitions for core/button overridden by the theme.' + ); + unregister_block_type( 'mycustom/block' ); + deactivate_plugins( 'test-plugin-theme-json/test-plugin-theme-json.php' ); + } } diff --git a/test/test-plugin-theme-json/test-plugin-theme-json.php b/test/test-plugin-theme-json/test-plugin-theme-json.php new file mode 100644 index 00000000000000..fd60b00d2dec62 --- /dev/null +++ b/test/test-plugin-theme-json/test-plugin-theme-json.php @@ -0,0 +1,10 @@ + Date: Wed, 27 Jul 2022 06:49:37 -0400 Subject: [PATCH 4/4] Eliminate unnecessary additional loop. --- ...class-wp-theme-json-resolver-gutenberg.php | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php index 183b6ee39df121..fa700dd478310a 100644 --- a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php @@ -150,25 +150,22 @@ public static function get_plugin_theme_data() { if ( null === static::$plugins ) { $plugins_data = array(); $active_plugin_paths = wp_get_active_and_valid_plugins(); + $plugin_json = new WP_Theme_JSON_Gutenberg(); foreach ( $active_plugin_paths as $path ) { $config = static::read_json_file( dirname( $path ) . '/theme.json' ); if ( ! empty( $config ) ) { + $plugin_meta_data = get_plugin_data( $path, false, false ); + if ( isset( $plugin_meta_data['TextDomain'] ) ) { + $config = static::translate( $config, $plugin_meta_data['TextDomain'] ); + } + // TODO, this is where we could potentially introduce different merge + // strategies for plugin provided data. + $plugin_json->merge( + new WP_Theme_JSON_Gutenberg( $config ) + ); $plugins_data[ $path ] = $config; } } - // have configs from plugins, now let's register and merge. - $plugin_json = new WP_Theme_JSON_Gutenberg(); - foreach ( $plugins_data as $plugin_path => $plugin_config ) { - $plugin_meta_data = get_plugin_data( $plugin_path, false, false ); - if ( isset( $plugin_meta_data['TextDomain'] ) ) { - $plugin_config = static::translate( $plugin_config, $plugin_meta_data['TextDomain'] ); - } - // TODO, this is where we could potentially introduce different merge - // strategies for plugin provided data. - $plugin_json->merge( - new WP_Theme_JSON_Gutenberg( $plugin_config ) - ); - } static::$plugins = $plugin_json; } return static::$plugins;