diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 2e372fc32c1702..7313afeba91bca 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -3646,4 +3646,78 @@ private static function resolve_custom_css_format( $tree ) { return $tree; } + + /** + * Replaces CSS variables with their values in place. + * + * @since 6.3.0 + * @param array $styles CSS declarations to convert. + * @param array $values key => value pairs to use for replacement. + * @return array + */ + private static function convert_variables_to_value( $styles, $values ) { + foreach ( $styles as $key => $style ) { + if ( is_array( $style ) ) { + $styles[ $key ] = self::convert_variables_to_value( $style, $values ); + continue; + } + + if ( 0 <= strpos( $style, 'var(' ) ) { + // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group. + + $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts ); + + if ( $has_matches ) { + $resolved_style = $styles[ $key ]; + foreach ( $var_parts[1] as $index => $var_part ) { + $key_in_values = 'var(' . $var_part . ')'; + $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan). + $fallback = $var_parts[2][ $index ]; // the fallback value. + $resolved_style = str_replace( + array( + $rule_to_replace, + $fallback, + ), + array( + isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace, + isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback, + ), + $resolved_style + ); + } + $styles[ $key ] = $resolved_style; + } + } + } + + return $styles; + } + + /** + * Resolves the values of CSS variables in the given styles. + * + * @since 6.3.0 + * @param WP_Theme_JSON_Gutenberg $theme_json The theme json resolver. + * + * @return WP_Theme_JSON_Gutenberg The $theme_json with resolved variables. + */ + public static function resolve_variables( $theme_json ) { + $settings = $theme_json->get_settings(); + $styles = $theme_json->get_raw_data()['styles']; + $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS ); + $theme_vars = static::compute_theme_vars( $settings ); + $vars = array_reduce( + array_merge( $preset_vars, $theme_vars ), + function( $carry, $item ) { + $name = $item['name']; + $carry[ "var({$name})" ] = $item['value']; + return $carry; + }, + array() + ); + + $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); + return $theme_json; + } + } diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index 309f01ca2bdd31..35ff4e13d61a75 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -238,6 +238,8 @@ function _gutenberg_clean_theme_json_caches() { * @since 5.9.0 * @since 6.3.0 the internal link format "var:preset|color|secondary" is resolved * to "var(--wp--preset--font-size--small)" so consumers don't have to. + * @since 6.3.0 `transforms` is now usable in the `context` parameter. In case [`transforms`]['resolve_variables'] + * is defined, variables are resolved to their value in the styles. * * @param array $path Path to the specific style to retrieve. Optional. * If empty, will return all styles. @@ -249,6 +251,9 @@ function _gutenberg_clean_theme_json_caches() { * @type string $origin Which origin to take data from. * Valid values are 'all' (core, theme, and user) or 'base' (core and theme). * If empty or unknown, 'all' is used. + * @type array $transforms Which transformation(s) to apply. + * Valid value is array( 'resolve-variables' ). + * If defined, variables are resolved to their value in the styles. * } * @return array The styles to retrieve. */ @@ -261,8 +266,16 @@ function gutenberg_get_global_styles( $path = array(), $context = array() ) { if ( isset( $context['origin'] ) && 'base' === $context['origin'] ) { $origin = 'theme'; } - $styles = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( $origin )->get_raw_data()['styles']; + $resolve_variables = isset( $context['transforms'] ) + && is_array( $context['transforms'] ) + && in_array( 'resolve-variables', $context['transforms'], true ); + + $merged_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( $origin ); + if ( $resolve_variables ) { + $merged_data = WP_Theme_JSON_Gutenberg::resolve_variables( $merged_data ); + } + $styles = $merged_data->get_raw_data()['styles']; return _wp_array_get( $styles, $path, $styles ); } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 1e5147bd0ed632..bd1d578a1297b6 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2021,4 +2021,158 @@ public function test_internal_syntax_is_converted_to_css_variables() { $this->assertEquals( 'var(--wp--preset--color--s)', $styles['blocks']['core/quote']['variations']['plain']['color']['background'], 'Style variations: Assert the internal variables are convert to CSS custom variables.' ); } + + public function test_resolve_variables() { + $primary_color = '#9DFF20'; + $secondary_color = '#9DFF21'; + $contrast_color = '#000'; + $raw_color_value = '#efefef'; + $large_font = '18px'; + $small_font = '12px'; + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'color' => $primary_color, + 'name' => 'Primary', + 'slug' => 'primary', + ), + array( + 'color' => $secondary_color, + 'name' => 'Secondary', + 'slug' => 'secondary', + ), + array( + 'color' => $contrast_color, + 'name' => 'Contrast', + 'slug' => 'contrast', + ), + ), + ), + ), + 'typography' => array( + 'fontSizes' => array( + array( + 'size' => $small_font, + 'name' => 'Font size small', + 'slug' => 'small', + ), + array( + 'size' => $large_font, + 'name' => 'Font size large', + 'slug' => 'large', + ), + ), + ), + ), + 'styles' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--primary)', + 'text' => $raw_color_value, + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'text' => 'var(--wp--preset--color--contrast)', + ), + 'typography' => array( + 'fontSize' => 'var(--wp--preset--font-size--small)', + ), + ), + ), + 'blocks' => array( + 'core/post-terms' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--small)' ), + 'color' => array( 'background' => $raw_color_value ), + ), + 'core/more' => array( + 'typography' => array( 'fontSize' => 'var(--undefined--font-size--small)' ), + 'color' => array( 'background' => 'linear-gradient(90deg, var(--wp--preset--color--primary) 0%, var(--wp--preset--color--secondary) 35%, var(--wp--undefined--color--secondary) 100%)' ), + ), + 'core/comment-content' => array( + 'typography' => array( 'fontSize' => 'calc(var(--wp--preset--font-size--small, 12px) + 20px)' ), + 'color' => array( + 'text' => 'var(--wp--preset--color--primary, red)', + 'background' => 'var(--wp--preset--color--primary, var(--wp--preset--font-size--secondary))', + 'link' => 'var(--undefined--color--primary, var(--wp--preset--font-size--secondary))', + ), + ), + 'core/comments' => array( + 'color' => array( + 'text' => 'var(--undefined--color--primary, var(--wp--preset--font-size--small))', + 'background' => 'var(--wp--preset--color--primary, var(--undefined--color--primary))', + ), + ), + 'core/navigation' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--primary)', + 'text' => 'var(--wp--preset--color--secondary)', + ), + 'typography' => array( + 'fontSize' => 'var(--wp--preset--font-size--large)', + ), + ), + ), + ), + 'core/quote' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--large)' ), + 'color' => array( 'background' => 'var(--wp--preset--color--primary)' ), + 'variations' => array( + 'plain' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--small)' ), + 'color' => array( 'background' => 'var(--wp--preset--color--secondary)' ), + ), + ), + ), + ), + ), + ) + ); + + $styles = $theme_json::resolve_variables( $theme_json )->get_raw_data()['styles']; + + $this->assertEquals( $primary_color, $styles['color']['background'], 'Top level: Assert values are converted' ); + $this->assertEquals( $raw_color_value, $styles['color']['text'], 'Top level: Assert raw values stay intact' ); + + $this->assertEquals( $contrast_color, $styles['elements']['button']['color']['text'], 'Elements: color' ); + $this->assertEquals( $small_font, $styles['elements']['button']['typography']['fontSize'], 'Elements: font-size' ); + + $this->assertEquals( $large_font, $styles['blocks']['core/quote']['typography']['fontSize'], 'Blocks: font-size' ); + $this->assertEquals( $primary_color, $styles['blocks']['core/quote']['color']['background'], 'Blocks: color' ); + $this->assertEquals( $raw_color_value, $styles['blocks']['core/post-terms']['color']['background'], 'Blocks: Raw color value stays intact' ); + $this->assertEquals( $small_font, $styles['blocks']['core/post-terms']['typography']['fontSize'], 'Block core/post-terms: font-size' ); + $this->assertEquals( + "linear-gradient(90deg, $primary_color 0%, $secondary_color 35%, var(--wp--undefined--color--secondary) 100%)", + $styles['blocks']['core/more']['color']['background'], + 'Blocks: multiple colors and undefined color' + ); + $this->assertEquals( 'var(--undefined--font-size--small)', $styles['blocks']['core/more']['typography']['fontSize'], 'Blocks: undefined font-size ' ); + $this->assertEquals( "calc($small_font + 20px)", $styles['blocks']['core/comment-content']['typography']['fontSize'], 'Blocks: font-size in random place' ); + $this->assertEquals( $primary_color, $styles['blocks']['core/comment-content']['color']['text'], 'Blocks: text color with fallback' ); + $this->assertEquals( $primary_color, $styles['blocks']['core/comment-content']['color']['background'], 'Blocks: background color with var as fallback' ); + $this->assertEquals( $primary_color, $styles['blocks']['core/navigation']['elements']['link']['color']['background'], 'Block element: background color' ); + $this->assertEquals( $secondary_color, $styles['blocks']['core/navigation']['elements']['link']['color']['text'], 'Block element: text color' ); + $this->assertEquals( $large_font, $styles['blocks']['core/navigation']['elements']['link']['typography']['fontSize'], 'Block element: font-size' ); + + $this->assertEquals( + "var(--undefined--color--primary, $small_font)", + $styles['blocks']['core/comments']['color']['text'], + 'Blocks: text color with undefined var and fallback' + ); + $this->assertEquals( + $primary_color, + $styles['blocks']['core/comments']['color']['background'], + 'Blocks: background color with variable and undefined fallback' + ); + + $this->assertEquals( $small_font, $styles['blocks']['core/quote']['variations']['plain']['typography']['fontSize'], 'Block variations: font-size' ); + $this->assertEquals( $secondary_color, $styles['blocks']['core/quote']['variations']['plain']['color']['background'], 'Block variations: color' ); + } + }