From eea6c51864b28ad4a8ea5d0daafeeee0566f06d7 Mon Sep 17 00:00:00 2001 From: punitverma123 Date: Mon, 20 Apr 2026 11:07:21 +0530 Subject: [PATCH 1/3] feat: remove custom- prefix from blank theme color slugs (#821) --- includes/class-create-block-theme-api.php | 4 +- includes/create-theme/resolver_additions.php | 161 +++++++++++++++---- includes/create-theme/theme-json.php | 4 +- src/editor-sidebar/save-panel.js | 21 +++ tests/class-create-block-theme-test-case.php | 34 ++++ tests/test-theme-colors.php | 152 +++++++++++++++++ tests/test-theme-fonts.php | 32 +--- 7 files changed, 344 insertions(+), 64 deletions(-) create mode 100644 tests/class-create-block-theme-test-case.php create mode 100644 tests/test-theme-colors.php diff --git a/includes/class-create-block-theme-api.php b/includes/class-create-block-theme-api.php index 12c42522..a81e09ba 100644 --- a/includes/class-create-block-theme-api.php +++ b/includes/class-create-block-theme-api.php @@ -369,9 +369,9 @@ function rest_save_theme( $request ) { if ( isset( $options['saveStyle'] ) && true === $options['saveStyle'] ) { if ( is_child_theme() ) { - CBT_Theme_JSON::add_theme_json_to_local( 'current', null, null, $options ); + CBT_Theme_JSON::add_theme_json_to_local( 'current', $options ); } else { - CBT_Theme_JSON::add_theme_json_to_local( 'all', null, null, $options ); + CBT_Theme_JSON::add_theme_json_to_local( 'all', $options ); } CBT_Theme_Styles::clear_user_styles_customizations(); } diff --git a/includes/create-theme/resolver_additions.php b/includes/create-theme/resolver_additions.php index 21117cf9..a28b4316 100644 --- a/includes/create-theme/resolver_additions.php +++ b/includes/create-theme/resolver_additions.php @@ -15,65 +15,53 @@ class CBT_Theme_JSON_Resolver extends WP_Theme_JSON_Resolver { * * @param string $content ['all', 'current', 'user'] Determines which settings content to include in the export. * @param array $extra_theme_data Any theme json extra data to be included in the export. + * @param array $options Export options. * All options include user settings. * 'current' will include settings from the currently installed theme but NOT from the parent theme. * 'all' will include settings from the current theme as well as the parent theme (if it has one) * 'variation' will include just the user custom styles and settings. */ - public static function export_theme_data( $content, $extra_theme_data = null ) { + public static function export_theme_data( $content, $extra_theme_data = null, $options = array() ) { $current_theme = wp_get_theme(); - if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { - $theme = new WP_Theme_JSON_Gutenberg(); - } else { - $theme = new WP_Theme_JSON(); - } + $theme = static::create_theme_json(); if ( 'all' === $content && $current_theme->parent() ) { // Get parent theme.json. - $parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) ); + $parent_theme_json_data = static::get_theme_file_contents( true ); $parent_theme_json_data = static::translate( $parent_theme_json_data, $current_theme->parent()->get( 'TextDomain' ) ); // Get the schema from the parent JSON. - $schema = $parent_theme_json_data['$schema']; - if ( array_key_exists( 'schema', $parent_theme_json_data ) ) { + if ( array_key_exists( '$schema', $parent_theme_json_data ) ) { $schema = $parent_theme_json_data['$schema']; } - if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { - $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); - } else { - $parent_theme = new WP_Theme_JSON( $parent_theme_json_data ); - } + $parent_theme = static::create_theme_json( $parent_theme_json_data ); $theme->merge( $parent_theme ); } if ( 'all' === $content || 'current' === $content ) { - $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); + $theme_json_data = static::get_theme_file_contents(); $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); // Get the schema from the parent JSON. - if ( array_key_exists( 'schema', $theme_json_data ) ) { + if ( array_key_exists( '$schema', $theme_json_data ) ) { $schema = $theme_json_data['$schema']; } - if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { - $theme_theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); - } else { - $theme_theme = new WP_Theme_JSON( $theme_json_data ); - } + $theme_theme = static::create_theme_json( $theme_json_data ); $theme->merge( $theme_theme ); } - // Merge the User Data - $theme->merge( static::get_user_data() ); + // Merge the User Data. + $user_data = static::get_user_data(); + if ( ! empty( $options['removeCustomColorPrefix'] ) ) { + $user_data = static::maybe_remove_custom_prefix_from_user_color_palette_slugs( $user_data ); + } + $theme->merge( $user_data ); // Merge the extra theme data received as a parameter if ( ! empty( $extra_theme_data ) ) { - if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { - $extra_data = new WP_Theme_JSON_Gutenberg( $extra_theme_data ); - } else { - $extra_data = new WP_Theme_JSON( $extra_theme_data ); - } + $extra_data = static::create_theme_json( $extra_theme_data ); $theme->merge( $extra_data ); } @@ -102,12 +90,123 @@ public static function export_theme_data( $content, $extra_theme_data = null ) { return static::stringify( $data ); } + /** + * Remove the custom- prefix from user-created color slugs when the theme + * does not define its own color palette yet. + * + * WordPress stores colors added in the editor as custom presets. When a + * blank theme has no palette, those first custom colors become the theme + * palette after saving, so their generated custom- prefix is redundant. + * + * @param WP_Theme_JSON $user_data User theme JSON data. + * @return WP_Theme_JSON User theme JSON data, with normalized color slugs when applicable. + */ + private static function maybe_remove_custom_prefix_from_user_color_palette_slugs( $user_data ) { + $theme_json_data = static::get_theme_file_contents(); + $theme_palette = $theme_json_data['settings']['color']['palette'] ?? null; + + if ( ! empty( $theme_palette ) ) { + return $user_data; + } + + $raw_user_data = $user_data->get_raw_data(); + $custom_palette = $raw_user_data['settings']['color']['palette']['custom'] ?? null; + + if ( empty( $custom_palette ) || ! is_array( $custom_palette ) ) { + return $user_data; + } + + $existing_slugs = array_filter( array_column( $custom_palette, 'slug' ) ); + $normalized_slugs = array(); + $slug_replacements = array(); + + foreach ( $custom_palette as $index => $color ) { + $color_slug = $color['slug'] ?? ''; + + if ( empty( $color_slug ) || 0 !== strpos( $color_slug, 'custom-' ) ) { + continue; + } + + $slug_without_prefix = substr( $color_slug, strlen( 'custom-' ) ); + + if ( + '' === $slug_without_prefix || + in_array( $slug_without_prefix, $existing_slugs, true ) || + in_array( $slug_without_prefix, $normalized_slugs, true ) + ) { + continue; + } + + $custom_palette[ $index ]['slug'] = $slug_without_prefix; + $slug_replacements[ $color_slug ] = $slug_without_prefix; + $normalized_slugs[] = $slug_without_prefix; + } + + if ( empty( $slug_replacements ) ) { + return $user_data; + } + + $raw_user_data['settings']['color']['palette']['custom'] = $custom_palette; + + if ( isset( $raw_user_data['styles'] ) ) { + static::replace_color_slug_references( $raw_user_data['styles'], $slug_replacements ); + } + + return static::create_theme_json( $raw_user_data ); + } + + /** + * Create the appropriate theme JSON object for the current environment. + * + * @param array $data Theme JSON data. + * @return WP_Theme_JSON Theme JSON object. + */ + private static function create_theme_json( $data = array() ) { + return class_exists( 'WP_Theme_JSON_Gutenberg' ) + ? new WP_Theme_JSON_Gutenberg( $data ) + : new WP_Theme_JSON( $data ); + } + + /** + * Replace color slug references in style values after slug normalization. + * + * @param mixed $data Theme JSON style data. + * @param array $slug_replacements Slug replacements keyed by original slug. + */ + private static function replace_color_slug_references( &$data, $slug_replacements ) { + if ( is_array( $data ) ) { + foreach ( $data as &$value ) { + static::replace_color_slug_references( $value, $slug_replacements ); + } + unset( $value ); + return; + } + + if ( ! is_string( $data ) ) { + return; + } + + foreach ( $slug_replacements as $old_slug => $new_slug ) { + $data = str_replace( + array( + 'var:preset|color|' . $old_slug, + 'var(--wp--preset--color--' . $old_slug . ')', + ), + array( + 'var:preset|color|' . $new_slug, + 'var(--wp--preset--color--' . $new_slug . ')', + ), + $data + ); + } + } + /** * Get the user data. * * This is a copy of the parent function with the addition of the Gutenberg resolver. * - * @return array + * @return WP_Theme_JSON User theme JSON data. */ public static function get_user_data() { // Determine the correct method to retrieve user data @@ -128,8 +227,8 @@ public static function stringify( $data ) { return preg_replace( '~(?:^|\G)\h{4}~m', "\t", $data ); } - public static function get_theme_file_contents() { - $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); + public static function get_theme_file_contents( $parent = false ) { + $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', $parent ) ); return $theme_json_data; } diff --git a/includes/create-theme/theme-json.php b/includes/create-theme/theme-json.php index dfedb885..2b6f110c 100644 --- a/includes/create-theme/theme-json.php +++ b/includes/create-theme/theme-json.php @@ -2,10 +2,10 @@ class CBT_Theme_JSON { - public static function add_theme_json_to_local( $export_type ) { + public static function add_theme_json_to_local( $export_type, $options = array() ) { file_put_contents( get_stylesheet_directory() . '/theme.json', - CBT_Theme_JSON_Resolver::export_theme_data( $export_type ) + CBT_Theme_JSON_Resolver::export_theme_data( $export_type, null, $options ) ); } diff --git a/src/editor-sidebar/save-panel.js b/src/editor-sidebar/save-panel.js index 568cddf0..b9a0f912 100644 --- a/src/editor-sidebar/save-panel.js +++ b/src/editor-sidebar/save-panel.js @@ -37,6 +37,8 @@ export const SaveThemePanel = () => { _preference?.processOnlySavedTemplates ?? true, savePatterns: _preference?.savePatterns ?? true, saveFonts: _preference?.saveFonts ?? true, + removeCustomColorPrefix: + _preference?.removeCustomColorPrefix ?? false, removeNavRefs: _preference?.removeNavRefs ?? false, localizeText: _preference?.localizeText ?? false, localizeImages: _preference?.localizeImages ?? false, @@ -129,6 +131,25 @@ export const SaveThemePanel = () => { checked={ preference.saveStyle } onChange={ () => handleTogglePreference( 'saveStyle' ) } /> + + handleTogglePreference( 'removeCustomColorPrefix' ) + } + /> set_param( 'name', $test_theme_slug ); + $request->set_param( 'description', '' ); + $request->set_param( 'uri', '' ); + $request->set_param( 'author', '' ); + $request->set_param( 'author_uri', '' ); + $request->set_param( 'tags_custom', '' ); + $request->set_param( 'recommended_plugins', '' ); + + rest_do_request( $request ); + + CBT_Theme_JSON_Resolver::clean_cached_data(); + + return $test_theme_slug; + } + + protected function uninstall_theme( $theme_slug ) { + CBT_Theme_JSON_Resolver::write_user_settings( array() ); + delete_theme( $theme_slug ); + } +} diff --git a/tests/test-theme-colors.php b/tests/test-theme-colors.php new file mode 100644 index 00000000..bcd3c617 --- /dev/null +++ b/tests/test-theme-colors.php @@ -0,0 +1,152 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public function test_custom_color_prefix_is_removed_when_option_is_enabled_for_blank_theme() { + wp_set_current_user( self::$admin_id ); + + $test_theme_slug = $this->create_blank_theme(); + + $this->add_user_color_palette( + array( + array( + 'slug' => 'custom-base', + 'name' => 'Base', + 'color' => '#2B2B2B', + ), + array( + 'slug' => 'custom-contrast', + 'name' => 'Contrast', + 'color' => '#F5F0E8', + ), + ), + array( + 'color' => array( + 'background' => 'var:preset|color|custom-base', + 'text' => 'var(--wp--preset--color--custom-contrast)', + ), + ) + ); + + CBT_Theme_JSON::add_theme_json_to_local( 'all', array( 'removeCustomColorPrefix' => true ) ); + CBT_Theme_JSON_Resolver::clean_cached_data(); + + $theme_data = CBT_Theme_JSON_Resolver::get_theme_file_contents(); + + $this->assertEquals( 'base', $theme_data['settings']['color']['palette'][0]['slug'] ); + $this->assertEquals( 'contrast', $theme_data['settings']['color']['palette'][1]['slug'] ); + $this->assertEquals( 'var(--wp--preset--color--base)', $theme_data['styles']['color']['background'] ); + $this->assertEquals( 'var(--wp--preset--color--contrast)', $theme_data['styles']['color']['text'] ); + + $this->uninstall_theme( $test_theme_slug ); + } + + public function test_custom_color_prefix_is_preserved_by_default_for_blank_theme() { + wp_set_current_user( self::$admin_id ); + + $test_theme_slug = $this->create_blank_theme(); + + $this->add_user_color_palette( + array( + array( + 'slug' => 'custom-base', + 'name' => 'Base', + 'color' => '#2B2B2B', + ), + array( + 'slug' => 'custom-contrast', + 'name' => 'Contrast', + 'color' => '#F5F0E8', + ), + ), + array( + 'color' => array( + 'background' => 'var:preset|color|custom-base', + 'text' => 'var(--wp--preset--color--custom-contrast)', + ), + ) + ); + + CBT_Theme_JSON::add_theme_json_to_local( 'all' ); + CBT_Theme_JSON_Resolver::clean_cached_data(); + + $theme_data = CBT_Theme_JSON_Resolver::get_theme_file_contents(); + + $this->assertEquals( 'custom-base', $theme_data['settings']['color']['palette'][0]['slug'] ); + $this->assertEquals( 'custom-contrast', $theme_data['settings']['color']['palette'][1]['slug'] ); + $this->assertEquals( 'var(--wp--preset--color--custom-base)', $theme_data['styles']['color']['background'] ); + $this->assertEquals( 'var(--wp--preset--color--custom-contrast)', $theme_data['styles']['color']['text'] ); + + $this->uninstall_theme( $test_theme_slug ); + } + + public function test_custom_color_prefix_is_preserved_when_theme_already_has_palette() { + wp_set_current_user( self::$admin_id ); + + $test_theme_slug = $this->create_blank_theme(); + + $theme_json = CBT_Theme_JSON_Resolver::get_theme_file_contents(); + $theme_json['settings']['color']['palette'] = array( + array( + 'slug' => 'base', + 'name' => 'Base', + 'color' => '#2B2B2B', + ), + ); + CBT_Theme_JSON_Resolver::write_theme_file_contents( $theme_json ); + + $this->add_user_color_palette( + array( + array( + 'slug' => 'custom-accent', + 'name' => 'Accent', + 'color' => '#C8A96E', + ), + ) + ); + + CBT_Theme_JSON::add_theme_json_to_local( 'all', array( 'removeCustomColorPrefix' => true ) ); + CBT_Theme_JSON_Resolver::clean_cached_data(); + + $theme_data = CBT_Theme_JSON_Resolver::get_theme_data()->get_settings(); + + $this->assertEquals( 'base', $theme_data['color']['palette']['theme'][0]['slug'] ); + $this->assertEquals( 'custom-accent', $theme_data['color']['palette']['theme'][1]['slug'] ); + + $this->uninstall_theme( $test_theme_slug ); + } + + private function add_user_color_palette( $palette, $styles = array() ) { + $settings = array( + 'color' => array( + 'palette' => array( + 'custom' => $palette, + ), + ), + ); + + $global_styles_id = CBT_Theme_JSON_Resolver::get_user_global_styles_post_id(); + $request = new WP_REST_Request( 'POST', '/wp/v2/global-styles/' . $global_styles_id ); + $request->set_param( 'settings', $settings ); + $request->set_param( 'styles', $styles ); + rest_do_request( $request ); + + CBT_Theme_JSON_Resolver::clean_cached_data(); + } +} diff --git a/tests/test-theme-fonts.php b/tests/test-theme-fonts.php index 3d5810b0..bea641d2 100644 --- a/tests/test-theme-fonts.php +++ b/tests/test-theme-fonts.php @@ -4,7 +4,9 @@ * * @package Create_Block_Theme */ -class Test_Create_Block_Theme_Fonts extends WP_UnitTestCase { +require_once __DIR__ . '/class-create-block-theme-test-case.php'; + +class Test_Create_Block_Theme_Fonts extends Create_Block_Theme_Test_Case { protected static $admin_id; protected static $editor_id; @@ -380,33 +382,6 @@ private function save_theme() { CBT_Theme_Fonts::persist_font_settings(); } - private function create_blank_theme() { - - $test_theme_slug = 'cbttesttheme'; - - delete_theme( $test_theme_slug ); - - $request = new WP_REST_Request( 'POST', '/create-block-theme/v1/create-blank' ); - $request->set_param( 'name', $test_theme_slug ); - $request->set_param( 'description', '' ); - $request->set_param( 'uri', '' ); - $request->set_param( 'author', '' ); - $request->set_param( 'author_uri', '' ); - $request->set_param( 'tags_custom', '' ); - $request->set_param( 'recommended_plugins', '' ); - - rest_do_request( $request ); - - CBT_Theme_JSON_Resolver::clean_cached_data(); - - return $test_theme_slug; - } - - private function uninstall_theme( $theme_slug ) { - CBT_Theme_JSON_Resolver::write_user_settings( array() ); - delete_theme( $theme_slug ); - } - private function activate_user_font() { $font_dir = wp_get_font_dir(); @@ -499,4 +474,3 @@ public function test_make_filename_from_fontface() { $this->assertEquals( $expected, $actual ); } } - From 0f673f1c15838d6bd4fde26601f6502d60dae3cc Mon Sep 17 00:00:00 2001 From: punitverma123 Date: Mon, 20 Apr 2026 14:28:34 +0530 Subject: [PATCH 2/3] feat: update SaveThemePanel to add option for removing custom- prefix from color slugs --- src/editor-sidebar/save-panel.js | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/editor-sidebar/save-panel.js b/src/editor-sidebar/save-panel.js index b9a0f912..2cd23fb4 100644 --- a/src/editor-sidebar/save-panel.js +++ b/src/editor-sidebar/save-panel.js @@ -131,25 +131,6 @@ export const SaveThemePanel = () => { checked={ preference.saveStyle } onChange={ () => handleTogglePreference( 'saveStyle' ) } /> - - handleTogglePreference( 'removeCustomColorPrefix' ) - } - /> { handleTogglePreference( 'removeTaxQuery' ) } /> + + handleTogglePreference( 'removeCustomColorPrefix' ) + } + />