From 1425211f0394cf0806dc0ec1c072203678e4ec37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= Date: Thu, 21 May 2020 10:00:19 +0200 Subject: [PATCH] Block Style System: managed CSS for Global Styles (#20290) This PR advances the use of experimental-theme.json by themes. This adds: - the ability to add presets (color, font-size, gradients) via the theme.json file - the presets declared by the theme (via this file or via functions.php) are now output as CSS Custom Properties available for themes to use - declare a set of "managed CSS" for some blocks, to match what a user can do in the editor --- experimental-default-global-styles.json | 129 ++++++- lib/global-styles.php | 442 +++++++++++++++++------- 2 files changed, 437 insertions(+), 134 deletions(-) diff --git a/experimental-default-global-styles.json b/experimental-default-global-styles.json index da4314beaaf32..d4687a6a46ac6 100644 --- a/experimental-default-global-styles.json +++ b/experimental-default-global-styles.json @@ -1,7 +1,128 @@ { - "color": { - "primary": "#52accc", - "background": "white", - "text": "black" + "global": { + "presets": { + "color": [ + { + "slug": "black", + "value": "#000000" + }, + { + "slug": "cyan-bluish-gray", + "value": "#abb8c3" + }, + { + "slug": "light-green-cyan", + "value": "#7bdcb5" + }, + { + "slug": "luminous-vivid-amber", + "value": "#fcb900" + }, + { + "slug": "luminous-vivid-orange", + "value": "#ff6900" + }, + { + "slug": "pale-cyan-blue", + "value": "#8ed1fc" + }, + { + "slug": "pale-pink", + "value": "#f78da7" + }, + { + "slug": "vivid-cyan-blue", + "value": "#0693e3" + }, + { + "slug": "vivid-green-cyan", + "value": "#00d084" + }, + { + "slug": "vivid-purple", + "value": "#9b51e0" + }, + { + "slug": "vivid-red", + "value": "#cf2e2e" + }, + { + "slug": "white", + "value": "#ffffff" + } + ], + "font-size": [ + { + "slug": "small", + "value": 13 + }, + { + "slug": "normal", + "value": 16 + }, + { + "slug": "medium", + "value": 20 + }, + { + "slug": "large", + "value": 36 + }, + { + "slug": "huge", + "value": 48 + } + ], + "gradient": [ + { + "slug": "blush-bordeaux", + "value": "linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%)" + }, + { + "slug": "blush-light-purple", + "value": "linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%)" + }, + { + "slug": "cool-to-warm-spectrum", + "value": "linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%)" + }, + { + "slug": "electric-grass", + "value": "linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%)" + }, + { + "slug": "light-green-cyan-to-vivid-green-cyan", + "value": "linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)" + }, + { + "slug": "luminous-dusk", + "value": "linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%)" + }, + { + "slug": "luminous-vivid-amber-to-luminous-vivid-orange", + "value": "linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%)" + }, + { + "slug": "luminous-vivid-orange-to-vivid-red", + "value": "linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%)" + }, + { + "slug": "midnight", + "value": "linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%)" + }, + { + "slug": "pale-ocean", + "value": "linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%)" + }, + { + "slug": "very-light-gray-to-cyan-bluish-gray", + "value": "linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%)" + }, + { + "slug": "vivid-cyan-blue-to-vivid-purple", + "value": "linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)" + } + ] + } } } diff --git a/lib/global-styles.php b/lib/global-styles.php index 8101bf41707f5..0dc76e7776acc 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -1,34 +1,56 @@ $value ) { - $new_key = $prefix . str_replace( '/', '-', $key ); + foreach ( $tree as $property => $value ) { + $new_key = $prefix . str_replace( '/', '-', $property ); if ( is_array( $value ) ) { $new_prefix = $new_key . $token; @@ -44,55 +66,56 @@ function gutenberg_experimental_global_styles_get_css_vars( $global_styles, $pre } /** - * Returns an array containing the Global Styles - * found in a file, or a void array if none found. + * Processes a file that adheres to the theme.json + * schema and returns an array with its contents, + * or a void array if none found. * - * @param string $global_styles_path Path to file. - * @return array Global Styles tree. + * @param string $file_path Path to file. + * @return array Contents that adhere to the theme.json schema. */ -function gutenberg_experimental_global_styles_get_from_file( $global_styles_path ) { - $global_styles = array(); - if ( file_exists( $global_styles_path ) ) { +function gutenberg_experimental_global_styles_get_from_file( $file_path ) { + $config = array(); + if ( file_exists( $file_path ) ) { $decoded_file = json_decode( - file_get_contents( $global_styles_path ), + file_get_contents( $file_path ), true ); if ( is_array( $decoded_file ) ) { - $global_styles = $decoded_file; + $config = $decoded_file; } } - return $global_styles; + return $config; } /** - * Returns an array containing the user's Global Styles - * or a void array if none. + * Returns the user's origin config. * - * @return array Global Styles tree. + * @return array Config that adheres to the theme.json schema. */ function gutenberg_experimental_global_styles_get_user() { - $global_styles = array(); - $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish' ) ); + $config = array(); + $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish' ) ); if ( array_key_exists( 'post_content', $user_cpt ) ) { $decoded_data = json_decode( $user_cpt['post_content'], true ); if ( is_array( $decoded_data ) ) { - $global_styles = $decoded_data; + $config = $decoded_data; } } - return $global_styles; + return $config; } /** - * Returns the CPT that contains the user's Global Styles + * Returns the CPT that contains the user's origin config * for the current theme or a void array if none found. + * * It can also create and return a new draft CPT. * - * @param array $post_status_filter Filter CPT by post status. - * By default, only fetches published posts. - * @param bool $should_create_draft Whether a new draft should be created - * if no CPT was found. False by default. - * @return array Custom Post Type for the user's Global Styles. + * @param array $post_status_filter Filter CPT by post status. + * ['publish'] by default, so it only fetches published posts. + * @param bool $should_create_draft Whether a new draft should be created if no CPT was found. + * False by default. + * @return array Custom Post Type for the user's origin config. */ function gutenberg_experimental_global_styles_get_user_cpt( $post_status_filter = array( 'publish' ), $should_create_draft = false ) { $user_cpt = array(); @@ -128,9 +151,9 @@ function gutenberg_experimental_global_styles_get_user_cpt( $post_status_filter } /** - * Returns the post ID of the CPT containing the user's Global Styles. + * Returns the post ID of the CPT containing the user's origin config. * - * @return integer Custom Post Type ID for the user's Global Styles. + * @return integer */ function gutenberg_experimental_global_styles_get_user_cpt_id() { $user_cpt_id = null; @@ -142,160 +165,320 @@ function gutenberg_experimental_global_styles_get_user_cpt_id() { } /** - * Return core's Global Styles. + * Return core's origin config. * - * @return array Global Styles tree. + * @return array Config that adheres to the theme.json schema. */ function gutenberg_experimental_global_styles_get_core() { - return gutenberg_experimental_global_styles_get_from_file( + $config = gutenberg_experimental_global_styles_get_from_file( dirname( dirname( __FILE__ ) ) . '/experimental-default-global-styles.json' ); + + return $config; } /** - * Return theme's Global Styles. It also fetches the editor palettes - * declared via add_theme_support. + * Returns the theme presets registered via add_theme_support, if any. * - * @return array Global Styles tree. + * @return array Config that adheres to the theme.json schema. */ -function gutenberg_experimental_global_styles_get_theme() { - $theme_supports = array(); - // Take colors from declared theme support. +function gutenberg_experimental_global_styles_get_theme_presets() { + $theme_presets = array(); + $theme_colors = get_theme_support( 'editor-color-palette' )[0]; if ( is_array( $theme_colors ) ) { foreach ( $theme_colors as $color ) { - $theme_supports['preset']['color'][ $color['slug'] ] = $color['color']; + $theme_presets['global']['presets']['color'][] = array( + 'slug' => $color['slug'], + 'value' => $color['color'], + ); } } - // Take gradients from declared theme support. $theme_gradients = get_theme_support( 'editor-gradient-presets' )[0]; if ( is_array( $theme_gradients ) ) { foreach ( $theme_gradients as $gradient ) { - $theme_supports['preset']['gradient'][ $gradient['slug'] ] = $gradient['gradient']; + $theme_presets['global']['presets']['gradient'][] = array( + 'slug' => $gradient['slug'], + 'value' => $gradient['gradient'], + ); } } - // Take font-sizes from declared theme support. $theme_font_sizes = get_theme_support( 'editor-font-sizes' )[0]; if ( is_array( $theme_font_sizes ) ) { foreach ( $theme_font_sizes as $font_size ) { - $theme_supports['preset']['font-size'][ $font_size['slug'] ] = $font_size['size']; + $theme_presets['global']['presets']['font-size'][] = array( + 'slug' => $font_size['slug'], + 'value' => $font_size['size'], + ); } } + return $theme_presets; +} + +/** + * Returns the theme's origin config. + * + * It also fetches the existing presets the theme declared via add_theme_support + * and uses them if the theme hasn't declared any via theme.json. + * + * @return array Config that adheres to the theme.json schema. + */ +function gutenberg_experimental_global_styles_get_theme() { + $theme_presets = gutenberg_experimental_global_styles_get_theme_presets(); + $theme_config = gutenberg_experimental_global_styles_get_from_file( + locate_template( 'experimental-theme.json' ) + ); + /* * We want the presets declared in theme.json * to take precedence over the ones declared via add_theme_support. * - * However, at the moment, it's not clear how we're going to declare them - * in theme.json until we resolve issues related to i18n and - * unfold the proper theme.json hierarchy. See: + * Note that merging happens at the preset category level. Example: + * + * - if the theme declares a color palette via add_theme_support & + * a set of font sizes via theme.json, both will be included in the output. * - * https://github.com/wp-cli/i18n-command/pull/210 - * https://github.com/WordPress/gutenberg/issues/20588 + * - if the theme declares a color palette both via add_theme_support & + * via theme.json, the later takes precedence. * - * Hence, for simplicity, we take here the existing presets - * from the add_theme_support, if any. */ - return array_merge( - gutenberg_experimental_global_styles_get_from_file( - locate_template( 'experimental-theme.json' ) + $theme_config = gutenberg_experimental_global_styles_merge_trees( + $theme_presets, + $theme_config + ); + + return $theme_config; +} + +/** + * Retrieves the block data (selector/supports). + * + * @return array + */ +function gutenberg_experimental_global_styles_get_block_data() { + // TODO: this data should be taken from the block registry. + // + // At the moment this array replicates the current capabilities + // declared by blocks via __experimentalLineHeight, + // __experimentalColor, and __experimentalFontSize. + $block_data = array( + 'global' => array( + 'selector' => ':root', + 'supports' => array(), // By being blank, the 'global' section won't output any style yet. + ), + 'core/paragraph' => array( + 'selector' => 'p', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h1' => array( + 'selector' => 'h1', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h2' => array( + 'selector' => 'h2', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h3' => array( + 'selector' => 'h3', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h4' => array( + 'selector' => 'h4', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h5' => array( + 'selector' => 'h5', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/heading/h6' => array( + 'selector' => 'h6', + 'supports' => array( 'line-height', 'font-size', 'color' ), + ), + 'core/columns' => array( + 'selector' => '.wp-block-columns', + 'supports' => array( 'color' ), + ), + 'core/group' => array( + 'selector' => '.wp-block-group', + 'supports' => array( 'color' ), + ), + 'core/media-text' => array( + 'selector' => '.wp-block-media-text', + 'supports' => array( 'color' ), ), - $theme_supports ); + + return $block_data; } /** - * Takes a Global Styles tree and returns a CSS rule - * that contains the corresponding CSS custom properties. + * Takes a tree adhering to the theme.json schema and generates + * the corresponding stylesheet. + * + * @param array $tree Input tree. * - * @param array $global_styles Global Styles tree. - * @return string CSS rule. + * @return string Stylesheet. */ -function gutenberg_experimental_global_styles_resolver( $global_styles ) { - $css_rule = ''; - - $token = '--'; - $prefix = '--wp' . $token; - $css_vars = gutenberg_experimental_global_styles_get_css_vars( $global_styles, $prefix, $token ); - if ( empty( $css_vars ) ) { - return $css_rule; +function gutenberg_experimental_global_styles_resolver( $tree ) { + $stylesheet = ''; + $block_data = gutenberg_experimental_global_styles_get_block_data(); + foreach ( array_keys( $tree ) as $block_name ) { + if ( + ! array_key_exists( $block_name, $block_data ) || + ! array_key_exists( 'selector', $block_data[ $block_name ] ) || + ! array_key_exists( 'supports', $block_data[ $block_name ] ) + ) { + // Skip blocks that haven't declared support, + // because we don't know to process them. + continue; + } + + // Extract the relevant preset info before converting them to CSS Custom Properties. + foreach ( array_keys( $tree[ $block_name ]['presets'] ) as $preset_category ) { + $flattened_values = array(); + foreach ( $tree[ $block_name ]['presets'][ $preset_category ] as $preset_value ) { + $flattened_values[ $preset_value['slug'] ] = $preset_value['value']; + } + $tree[ $block_name ]['presets'][ $preset_category ] = $flattened_values; + } + + $token = '--'; + $prefix = '--wp--preset' . $token; + $css_variables = gutenberg_experimental_global_styles_get_css_vars( $tree[ $block_name ]['presets'], $prefix, $token ); + + $stylesheet .= gutenberg_experimental_global_styles_resolver_styles( + $block_data[ $block_name ]['selector'], + $block_data[ $block_name ]['supports'], + array_merge( $tree[ $block_name ]['styles'], $css_variables ) + ); } + return $stylesheet; +} - $css_rule = ":root {\n"; - foreach ( $css_vars as $var => $value ) { - $css_rule .= "\t" . $var . ': ' . $value . ";\n"; +/** + * Generates CSS declarations for a block. + * + * @param string $block_selector CSS selector for the block. + * @param array $block_supports A list of properties supported by the block. + * @param array $block_styles The list of properties/values to be converted to CSS. + * + * @return string The corresponding CSS rule. + */ +function gutenberg_experimental_global_styles_resolver_styles( $block_selector, $block_supports, $block_styles ) { + $css_rule = ''; + $css_declarations = ''; + foreach ( $block_styles as $property => $value ) { + // Only convert to CSS: + // + // 1) The style attributes the block has declared support for. + // 2) Any CSS custom property attached to the node. + if ( in_array( $property, $block_supports, true ) || strstr( $property, '--' ) ) { + $css_declarations .= "\t" . $property . ': ' . $value . ";\n"; + } + } + if ( '' !== $css_declarations ) { + $css_rule .= $block_selector . " {\n"; + $css_rule .= $css_declarations; + $css_rule .= "}\n"; } - $css_rule .= '}'; return $css_rule; } /** - * Fetches the Global Styles for each level (core, theme, user) - * and enqueues the resulting CSS custom properties for the editor. + * Helper function that merges trees that adhere to the theme.json schema. + * + * @param array $core Core origin. + * @param array $theme Theme origin. + * @param array $user User origin. An empty array by default. + * + * @return array The merged result. */ -function gutenberg_experimental_global_styles_enqueue_assets_editor() { - if ( gutenberg_experimental_global_styles_is_site_editor() ) { - gutenberg_experimental_global_styles_enqueue_assets(); +function gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user = array() ) { + $core = gutenberg_experimental_global_styles_normalize_schema( $core ); + $theme = gutenberg_experimental_global_styles_normalize_schema( $theme ); + $user = gutenberg_experimental_global_styles_normalize_schema( $user ); + $result = gutenberg_experimental_global_styles_normalize_schema( array() ); + + foreach ( array_keys( $core ) as $block_name ) { + foreach ( array( 'presets', 'styles', 'features' ) as $subtree ) { + $result[ $block_name ][ $subtree ] = array_merge( + $core[ $block_name ][ $subtree ], + $theme[ $block_name ][ $subtree ], + $user[ $block_name ][ $subtree ] + ); + } } + + return $result; } /** - * Fetches the Global Styles for each level (core, theme, user) - * and enqueues the resulting CSS custom properties. + * Given a tree, it normalizes it to the expected schema. + * + * @param array $tree Source tree to normalize. + * + * @return array Normalized tree. */ -function gutenberg_experimental_global_styles_enqueue_assets() { - if ( ! gutenberg_experimental_global_styles_has_theme_support() ) { - return; - } - - $global_styles = array_merge( - gutenberg_experimental_global_styles_get_core(), - gutenberg_experimental_global_styles_get_theme(), - gutenberg_experimental_global_styles_get_user() +function gutenberg_experimental_global_styles_normalize_schema( $tree ) { + $block_schema = array( + 'styles' => array(), + 'features' => array(), + 'presets' => array(), ); - $inline_style = gutenberg_experimental_global_styles_resolver( $global_styles ); - if ( empty( $inline_style ) ) { - return; + $normalized_tree = array(); + $block_data = gutenberg_experimental_global_styles_get_block_data(); + foreach ( array_keys( $block_data ) as $block_name ) { + $normalized_tree[ $block_name ] = $block_schema; } - wp_register_style( 'global-styles', false, array(), true, true ); - wp_add_inline_style( 'global-styles', $inline_style ); - wp_enqueue_style( 'global-styles' ); + $tree = array_merge_recursive( + $normalized_tree, + $tree + ); + + return $tree; } /** - * Adds class wp-gs to the frontend body class. + * Returns the stylesheet resulting of merging + * core's, theme's, and user's origins. * - * @param array $classes Existing body classes. - * @return array The filtered array of body classes. + * @return string */ -function gutenberg_experimental_global_styles_wp_gs_class_front_end( $classes ) { - if ( ! gutenberg_experimental_global_styles_has_theme_support() ) { - return $classes; - } +function gutenberg_experimental_global_styles_get_stylesheet() { + $gs_merged = array(); + $gs_core = gutenberg_experimental_global_styles_get_core(); + $gs_theme = gutenberg_experimental_global_styles_get_theme(); + $gs_user = gutenberg_experimental_global_styles_get_user(); + + $gs_merged = gutenberg_experimental_global_styles_merge_trees( $gs_core, $gs_theme, $gs_user ); - return array_merge( $classes, array( 'wp-gs' ) ); + $stylesheet = gutenberg_experimental_global_styles_resolver( $gs_merged ); + if ( empty( $stylesheet ) ) { + return; + } + return $stylesheet; } /** - * Adds class wp-gs to the block-editor body class. - * - * @param string $classes Existing body classes separated by space. - * @return string The filtered string of body classes. + * Fetches the preferences for each origin (core, theme, user) + * and enqueues the resulting stylesheet. */ -function gutenberg_experimental_global_styles_wp_gs_class_editor( $classes ) { - if ( - ! gutenberg_experimental_global_styles_has_theme_support() || - ! gutenberg_experimental_global_styles_is_site_editor() - ) { - return $classes; +function gutenberg_experimental_global_styles_enqueue_assets() { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { + return; } - return $classes . ' wp-gs'; + $stylesheet = gutenberg_experimental_global_styles_get_stylesheet(); + + wp_register_style( 'global-styles', false, array(), true, true ); + wp_add_inline_style( 'global-styles', $stylesheet ); + wp_enqueue_style( 'global-styles' ); } /** @@ -313,16 +496,14 @@ function gutenberg_experimental_global_styles_is_site_editor() { } /** - * Makes the base Global Styles (core, theme) - * and the ID of the CPT that stores the user's Global Styles - * available to the client, to be used for live rendering the styles. + * Adds the necessary data for the Global Styles client UI to the block settings. * * @param array $settings Existing block editor settings. * @return array New block editor settings */ function gutenberg_experimental_global_styles_settings( $settings ) { if ( - ! gutenberg_experimental_global_styles_has_theme_support() || + ! gutenberg_experimental_global_styles_has_theme_json_support() || ! gutenberg_experimental_global_styles_is_site_editor() ) { return $settings; @@ -330,21 +511,26 @@ function gutenberg_experimental_global_styles_settings( $settings ) { $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id(); - $global_styles = array_merge( + $global_styles = gutenberg_experimental_global_styles_merge_trees( gutenberg_experimental_global_styles_get_core(), gutenberg_experimental_global_styles_get_theme() ); $settings['__experimentalGlobalStylesBase'] = $global_styles; + // Add the styles for the editor via the settings + // so they get processed as if they were added via add_editor_styles: + // they will get the editor wrapper class. + $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet() ); + return $settings; } /** - * Registers a Custom Post Type to store the user's Global Styles. + * Registers a Custom Post Type to store the user's origin config. */ function gutenberg_experimental_global_styles_register_cpt() { - if ( ! gutenberg_experimental_global_styles_has_theme_support() ) { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { return; } @@ -375,10 +561,6 @@ function gutenberg_experimental_global_styles_register_cpt() { if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) ) { add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' ); - add_filter( 'body_class', 'gutenberg_experimental_global_styles_wp_gs_class_front_end' ); - add_filter( 'admin_body_class', 'gutenberg_experimental_global_styles_wp_gs_class_editor' ); add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings' ); - // enqueue_block_assets is not fired in edit-site, so we use separate back/front hooks instead. add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' ); - add_action( 'admin_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets_editor' ); }