diff --git a/lib/compat/wordpress-6.2/block-editor-settings.php b/lib/compat/wordpress-6.2/block-editor-settings.php new file mode 100644 index 00000000000000..7323d34eb667ce --- /dev/null +++ b/lib/compat/wordpress-6.2/block-editor-settings.php @@ -0,0 +1,28 @@ + gutenberg_get_global_stylesheet( array( 'custom-css' ) ), + '__unstableType' => 'user', + 'isGlobalStyles' => true, + ); + } + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_6_2', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php new file mode 100644 index 00000000000000..9f762dd961d058 --- /dev/null +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php @@ -0,0 +1,202 @@ +post_content, true ); + $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; + $config = array(); + if ( $is_global_styles_user_theme_json ) { + $config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data(); + } + + // Base fields for every post. + $data = array(); + $fields = $this->get_fields_for_response( $request ); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $post->ID; + } + + if ( rest_is_field_included( 'title', $fields ) ) { + $data['title'] = array(); + } + if ( rest_is_field_included( 'title.raw', $fields ) ) { + $data['title']['raw'] = $post->post_title; + } + if ( rest_is_field_included( 'title.rendered', $fields ) ) { + add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + + $data['title']['rendered'] = get_the_title( $post->ID ); + + remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + } + + if ( rest_is_field_included( 'settings', $fields ) ) { + $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass(); + } + + if ( rest_is_field_included( 'styles', $fields ) ) { + $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $post->ID ); + $response->add_links( $links ); + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions(); + $self = $links['self']['href']; + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } + } + + return $response; + } + + /** + * Updates a single global style config. + * + * @since 5.9.0 + * @since 6.2.0 Added validation of styles.css property. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function update_item( $request ) { + $post_before = $this->get_post( $request['id'] ); + if ( is_wp_error( $post_before ) ) { + return $post_before; + } + + $changes = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $changes ) ) { + return $changes; + } + + $result = wp_update_post( wp_slash( (array) $changes ), true, false ); + if ( is_wp_error( $result ) ) { + return $result; + } + + $post = get_post( $request['id'] ); + $fields_update = $this->update_additional_fields_for_object( $post, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + wp_after_insert_post( $post, true, $post_before ); + + $response = $this->prepare_item_for_response( $post, $request ); + + return rest_ensure_response( $response ); + } + /** + * Prepares a single global styles config for update. + * + * @since 5.9.0 + * @since 6.2.0 Added validation of styles.css property. + * + * @param WP_REST_Request $request Request object. + * @return stdClass Changes to pass to wp_update_post. + */ + protected function prepare_item_for_database( $request ) { + $changes = new stdClass(); + $changes->ID = $request['id']; + $post = get_post( $request['id'] ); + $existing_config = array(); + if ( $post ) { + $existing_config = json_decode( $post->post_content, true ); + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) || + ! $existing_config['isGlobalStylesUserThemeJSON'] ) { + $existing_config = array(); + } + } + if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { + $config = array(); + if ( isset( $request['styles'] ) ) { + $config['styles'] = $request['styles']; + $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] ); + if ( is_wp_error( $validate_custom_css ) ) { + return $validate_custom_css; + } + } elseif ( isset( $existing_config['styles'] ) ) { + $config['styles'] = $existing_config['styles']; + } + if ( isset( $request['settings'] ) ) { + $config['settings'] = $request['settings']; + } elseif ( isset( $existing_config['settings'] ) ) { + $config['settings'] = $existing_config['settings']; + } + $config['isGlobalStylesUserThemeJSON'] = true; + $config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA; + $changes->post_content = wp_json_encode( $config ); + } + // Post title. + if ( isset( $request['title'] ) ) { + if ( is_string( $request['title'] ) ) { + $changes->post_title = $request['title']; + } elseif ( ! empty( $request['title']['raw'] ) ) { + $changes->post_title = $request['title']['raw']; + } + } + return $changes; + } + + /** + * Validate style.css as valid CSS. + * + * Currently just checks for invalid markup. + * + * @since 6.2.0 + * + * @param string $css CSS to validate. + * @return true|WP_Error True if the input was validated, otherwise WP_Error. + */ + private function validate_custom_css( $css ) { + if ( preg_match( '# 400 ) + ); + } + return true; + } +} diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php index 3b368a10b7f303..fe01eb273066e2 100644 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php +++ b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php @@ -194,6 +194,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 { * @since 6.1.0 Added new side properties for `border`, * added new property `shadow`, * updated `blockGap` to be allowed at any level. + * @since 6.2.0 Added new property `css`. * @var array */ const VALID_STYLES = array( @@ -234,6 +235,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 { 'textDecoration' => null, 'textTransform' => null, ), + 'css' => null, ); /** @@ -277,4 +279,113 @@ protected static function remove_insecure_styles( $input ) { return $output; } + + /** + * Returns the stylesheet that results of processing + * the theme.json structure this object represents. + * + * @param array $types Types of styles to load. Will load all by default. It accepts: + * 'variables': only the CSS Custom Properties for presets & custom ones. + * 'styles': only the styles section in theme.json. + * 'presets': only the classes for the presets. + * 'custom-css': only the css from global styles.css. + * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. + * @param array $options An array of options for now used for internal purposes only (may change without notice). + * The options currently supported are 'scope' that makes sure all style are scoped to a given selector, + * and root_selector which overwrites and forces a given selector to be used on the root node. + * @return string Stylesheet. + */ + public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { + if ( null === $origins ) { + $origins = static::VALID_ORIGINS; + } + + if ( is_string( $types ) ) { + // Dispatch error and map old arguments to new ones. + _deprecated_argument( __FUNCTION__, '5.9' ); + if ( 'block_styles' === $types ) { + $types = array( 'styles', 'presets' ); + } elseif ( 'css_variables' === $types ) { + $types = array( 'variables' ); + } else { + $types = array( 'variables', 'styles', 'presets' ); + } + } + + $blocks_metadata = static::get_blocks_metadata(); + $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); + $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); + + $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); + $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); + + if ( ! empty( $options['scope'] ) ) { + foreach ( $setting_nodes as &$node ) { + $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); + } + foreach ( $style_nodes as &$node ) { + $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); + } + } + + if ( ! empty( $options['root_selector'] ) ) { + if ( false !== $root_settings_key ) { + $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; + } + if ( false !== $root_style_key ) { + $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector']; + } + } + + $stylesheet = ''; + + if ( in_array( 'variables', $types, true ) ) { + $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); + } + + if ( in_array( 'styles', $types, true ) ) { + if ( false !== $root_style_key ) { + $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); + } + $stylesheet .= $this->get_block_classes( $style_nodes ); + } elseif ( in_array( 'base-layout-styles', $types, true ) ) { + $root_selector = static::ROOT_BLOCK_SELECTOR; + $columns_selector = '.wp-block-columns'; + if ( ! empty( $options['scope'] ) ) { + $root_selector = static::scope_selector( $options['scope'], $root_selector ); + $columns_selector = static::scope_selector( $options['scope'], $columns_selector ); + } + if ( ! empty( $options['root_selector'] ) ) { + $root_selector = $options['root_selector']; + } + // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested. + // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value. + $base_styles_nodes = array( + array( + 'path' => array( 'styles' ), + 'selector' => $root_selector, + ), + array( + 'path' => array( 'styles', 'blocks', 'core/columns' ), + 'selector' => $columns_selector, + 'name' => 'core/columns', + ), + ); + + foreach ( $base_styles_nodes as $base_style_node ) { + $stylesheet .= $this->get_layout_styles( $base_style_node ); + } + } + + if ( in_array( 'presets', $types, true ) ) { + $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); + } + + // Load the custom CSS last so it has the highest specificity. + if ( in_array( 'custom-css', $types, true ) ) { + $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); + } + + return $stylesheet; + } } diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php index 2cce8cec7e46e1..110c8bac7b147c 100644 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php +++ b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php @@ -168,4 +168,63 @@ public static function get_merged_data( $origin = 'custom' ) { $result->set_spacing_sizes(); return $result; } + + /** + * Returns the user's origin config. + * + * @since 6.2 Added check for the WP_Theme_JSON_Gutenberg class to prevent $user + * values set in core fron overriding the new custom css values added to VALID_STYLES. + * This does not need to be backported to core as the new VALID_STYLES[css] value will + * be added to core with 6.2. + * + * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. + */ + public static function get_user_data() { + if ( null !== static::$user && static::$user instanceof WP_Theme_JSON_Gutenberg ) { + return static::$user; + } + + $config = array(); + $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); + + if ( array_key_exists( 'post_content', $user_cpt ) ) { + $decoded_data = json_decode( $user_cpt['post_content'], true ); + + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error ) { + trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); + /** + * Filters the data provided by the user for global styles & settings. + * + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + } + + // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. + // If is not true the content was not escaped and is not safe. + if ( + is_array( $decoded_data ) && + isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && + $decoded_data['isGlobalStylesUserThemeJSON'] + ) { + unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); + $config = $decoded_data; + } + } + + /** + * Filters the data provided by the user for global styles & settings. + * + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + + static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + + return static::$user; + } } diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 1bf1ea23b6417f..b2188a92474f63 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -85,7 +85,7 @@ function gutenberg_get_global_stylesheet( $types = array() ) { if ( empty( $types ) && ! $supports_theme_json ) { $types = array( 'variables', 'presets', 'base-layout-styles' ); } elseif ( empty( $types ) ) { - $types = array( 'variables', 'styles', 'presets' ); + $types = array( 'variables', 'styles', 'presets', 'custom-css' ); } /* diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php index 4541ce0ae158c6..4900a622b6c01b 100644 --- a/lib/compat/wordpress-6.2/rest-api.php +++ b/lib/compat/wordpress-6.2/rest-api.php @@ -92,3 +92,12 @@ function gutenberg_pattern_directory_collection_params_6_2( $query_params ) { return $query_params; } add_filter( 'rest_pattern_directory_collection_params', 'gutenberg_pattern_directory_collection_params_6_2' ); + +/** + * Registers the Global Styles REST API routes. + */ +function gutenberg_register_global_styles_endpoints() { + $editor_settings = new Gutenberg_REST_Global_Styles_Controller_6_2(); + $editor_settings->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); diff --git a/lib/load.php b/lib/load.php index 3d871e607d7fa5..b634b85b977e38 100644 --- a/lib/load.php +++ b/lib/load.php @@ -46,6 +46,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php'; require_once __DIR__ . '/compat/wordpress-6.2/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.2/block-patterns.php'; + require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php'; // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { @@ -60,8 +61,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; // WordPress 6.1 compat. -require __DIR__ . '/compat/wordpress-6.1/blocks.php'; require __DIR__ . '/compat/wordpress-6.1/block-editor-settings.php'; +require __DIR__ . '/compat/wordpress-6.1/blocks.php'; require __DIR__ . '/compat/wordpress-6.1/persisted-preferences.php'; require __DIR__ . '/compat/wordpress-6.1/get-global-styles-and-settings.php'; require __DIR__ . '/compat/wordpress-6.1/class-wp-theme-json-data-gutenberg.php'; @@ -83,6 +84,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.2/class-wp-theme-json-6-2.php'; require __DIR__ . '/compat/wordpress-6.2/edit-form-blocks.php'; require __DIR__ . '/compat/wordpress-6.2/site-editor.php'; +require __DIR__ . '/compat/wordpress-6.2/block-editor-settings.php'; // Experimental features. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WP 6.0's stopgap handler for Webfonts API. diff --git a/packages/edit-site/src/components/global-styles/custom-css.js b/packages/edit-site/src/components/global-styles/custom-css.js new file mode 100644 index 00000000000000..6c7d3de1cf06e5 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/custom-css.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { TextareaControl, Panel, PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useStyle } from './hooks'; + +function CustomCSSControl() { + const [ customCSS, setCustomCSS ] = useStyle( 'css' ); + const [ themeCSS ] = useStyle( 'css', null, 'base' ); + const ignoreThemeCustomCSS = '/* IgnoreThemeCustomCSS */'; + + // If there is custom css from theme.json show it in the edit box + // so the user can selectively overwrite it, rather than have the user CSS + // completely overwrite the theme CSS by default. + const themeCustomCSS = + ! customCSS && themeCSS + ? `/* ${ __( + 'Theme Custom CSS start' + ) } */\n${ themeCSS }\n/* ${ __( 'Theme Custom CSS end' ) } */` + : undefined; + + function handleOnChange( value ) { + // If there is theme custom CSS, but the user clears the input box then save the + // ignoreThemeCustomCSS string so that the theme custom CSS is not re-applied. + if ( themeCSS && value === '' ) { + setCustomCSS( ignoreThemeCustomCSS ); + return; + } + setCustomCSS( value ); + } + + const originalThemeCustomCSS = + themeCSS && customCSS && themeCustomCSS !== customCSS + ? themeCSS + : undefined; + + return ( + <> + handleOnChange( value ) } + rows={ 15 } + className="edit-site-global-styles__custom-css-input" + spellCheck={ false } + help={ __( + "Enter your custom CSS in the textarea and preview in the editor. Changes won't take effect until you've saved the template." + ) } + /> + { originalThemeCustomCSS && ( + + +
+							{ originalThemeCustomCSS }
+						
+
+
+ ) } + + ); +} + +export default CustomCSSControl; diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js index 86e8aa2d2a6413..c767f1a488cbfd 100644 --- a/packages/edit-site/src/components/global-styles/hooks.js +++ b/packages/edit-site/src/components/global-styles/hooks.js @@ -138,7 +138,11 @@ export function useStyle( path, blockName, source = 'all' ) { result = getValueFromVariable( mergedConfig, blockName, - get( userConfig, finalPath ) ?? get( baseConfig, finalPath ) + // The stlyes.css path is allowed to be empty, so don't revert to base if undefined. + finalPath === 'styles.css' + ? get( userConfig, finalPath ) + : get( userConfig, finalPath ) ?? + get( baseConfig, finalPath ) ); break; case 'user': diff --git a/packages/edit-site/src/components/global-styles/screen-css.js b/packages/edit-site/src/components/global-styles/screen-css.js new file mode 100644 index 00000000000000..74480a0677bd16 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-css.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { __experimentalVStack as VStack } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ScreenHeader from './header'; +import Subtitle from './subtitle'; +import CustomCSSControl from './custom-css'; + +function ScreenCSS() { + return ( + <> + +
+ + { __( 'ADDITIONAL CSS' ) } + + +
+ + ); +} + +export default ScreenCSS; diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/edit-site/src/components/global-styles/screen-root.js index 0674b56319c70b..ecb68d27b7f4d7 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -75,7 +75,8 @@ function ScreenRoot() { paddingTop={ 2 } /* * 13px matches the text inset of the NavigationButton (12px padding, plus the width of the button's border). - * This is an ad hoc override for this particular instance only and should be reconsidered before making into a pattern. + * This is an ad hoc override for this instance and the Addtional CSS option below. Other options for matching the + * the nav button inset should be looked at before reusing further. */ paddingX="13px" marginBottom={ 4 } @@ -98,6 +99,34 @@ function ScreenRoot() { + + + + + + { __( + 'Add your own CSS to customize the appearance and layout of your site.' + ) } + + + + + { __( 'Custom' ) } + + + + + ); } diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 16c9b9c418dcad..5616a068b594c8 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -31,7 +31,8 @@ $block-preview-height: 150px; } .edit-site-global-styles-screen-heading-color, -.edit-site-global-styles-screen-typography { +.edit-site-global-styles-screen-typography, +.edit-site-global-styles-screen-css { margin: $grid-unit-20; } @@ -133,3 +134,15 @@ $block-preview-height: 150px; border: $gray-200 $border-width solid; border-radius: $radius-block-ui; } + +.edit-site-global-styles__custom-css-input textarea { + font-family: $editor_html_font; +} + +.edit-site-global-styles__custom-css-theme-css { + width: 100%; + line-break: anywhere; + white-space: break-spaces; + max-height: 200px; + overflow-y: scroll; +} diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 532a211597a81f..d7c9eed7d8474b 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -27,6 +27,7 @@ import ScreenLayout from './screen-layout'; import ScreenStyleVariations from './screen-style-variations'; import ScreenBorder from './screen-border'; import StyleBook from '../style-book'; +import ScreenCSS from './screen-css'; function GlobalStylesNavigationScreen( { className, ...props } ) { return ( @@ -191,6 +192,9 @@ function GlobalStylesUI( { isStyleBookOpened, onCloseStyleBook } ) { { isStyleBookOpened && ( ) } + + + ); } diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index b32201fe2d2c22..d83693757b035c 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -919,6 +919,11 @@ export function useGlobalStylesOutput() { css: globalStyles, isGlobalStyles: true, }, + // Load custom CSS in own stylesheet so that any invalid CSS entered in the input won't break all the global styles in the editor. + { + css: mergedConfig.styles.css ?? '', + isGlobalStyles: true, + }, ]; return [ stylesheets, mergedConfig.settings, filters ]; diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 74306fa5d8ae32..a5413766ca7ddd 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -1497,4 +1497,18 @@ public function test_update_separator_declarations() { $this->assertEquals( $expected, $stylesheet ); } + + public function test_get_stylesheet_handles_custom_css() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'css' => 'body { color:purple; }', + ), + ) + ); + + $custom_css = 'body { color:purple; }'; + $this->assertEquals( $custom_css, $theme_json->get_stylesheet( array( 'custom-css' ) ) ); + } }