diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 5bb0c4fa944231..5499153c25c411 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -617,8 +617,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' * @return string Styles for the block. */ public function get_styles_for_block( $block_metadata ) { - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); $selector = $block_metadata['selector']; $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); @@ -626,10 +625,8 @@ public function get_styles_for_block( $block_metadata ) { // $block_metadata['path'] = array('styles','elements','link'); // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. // Skip non-element paths like just ['styles']. - $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); - - $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; - + $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; $element_pseudo_allowed = isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ? static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] : array(); // Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). @@ -654,8 +651,6 @@ function( $pseudo_selector ) use ( $selector ) { $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json ); } - $block_rules = ''; - // 1. Separate the ones who use the general selector // and the ones who use the duotone selector. $declarations_duotone = array(); @@ -666,20 +661,32 @@ function( $pseudo_selector ) use ( $selector ) { } } - /* - * Reset default browser margin on the root body element. - * This is set on the root selector **before** generating the ruleset - * from the `theme.json`. This is to ensure that if the `theme.json` declares - * `margin` in its `spacing` declaration for the `body` element then these - * user-generated values take precedence in the CSS cascade. - * @link https://github.com/WordPress/gutenberg/issues/36147. - */ + $block_rules = ''; + + // 2. Generate and append the rules that use the general selector. if ( static::ROOT_BLOCK_SELECTOR === $selector ) { + /* + * Reset default browser margin on the root body element. + * This is set on the root selector **before** generating the ruleset + * from the `theme.json`. This is to ensure that if the `theme.json` declares + * `margin` in its `spacing` declaration for the `body` element then these + * user-generated values take precedence in the CSS cascade. + * @link https://github.com/WordPress/gutenberg/issues/36147. + */ $block_rules .= 'body { margin: 0; }'; - } - // 2. Generate and append the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); + /* + Style engine is generating the root styles only. + This way we can iteratively introduce it to start generating styles.elements, and styles.blocks. + */ + $block_rules .= gutenberg_style_engine_generate_global_styles( + $node, + array( 'selector' => $selector ) + ); + + } else { + $block_rules .= static::to_ruleset( $selector, $declarations ); + } // 3. Generate and append the rules that use the duotone selector. if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { diff --git a/packages/style-engine/class-wp-style-engine-css-declarations.php b/packages/style-engine/class-wp-style-engine-css-declarations.php index bd7cbe616f68d5..13ec0d5142bf58 100644 --- a/packages/style-engine/class-wp-style-engine-css-declarations.php +++ b/packages/style-engine/class-wp-style-engine-css-declarations.php @@ -97,6 +97,7 @@ public function get_declarations() { public function get_declarations_string() { $declarations_array = $this->get_declarations(); $declarations_output = ''; + foreach ( $declarations_array as $property => $value ) { $filtered_declaration = esc_html( safecss_filter_attr( "{$property}: {$value}" ) ); if ( $filtered_declaration ) { diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index a140d9ba8c6b81..9524fff044b356 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -274,6 +274,20 @@ protected static function get_css_var_value( $style_value, $css_vars ) { return null; } + /** + * Using a given path, return a value from the $block_styles object. + * + * @param array $block_styles Styles from a block's attributes object. + * @param string $ref A dot syntax path to another value in the $block_styles object, e.g., `styles.color.text`. + * + * @return string|array A style value from the block styles object. + */ + protected static function get_ref_value( $block_styles = array(), $ref = '' ) { + $ref = preg_replace( '/^styles\./', '', $ref ); + $path = explode( '.', $ref ); + return _wp_array_get( $block_styles, $path, null ); + } + /** * Checks whether an incoming block style value is valid. * @@ -334,20 +348,22 @@ protected static function get_classnames( $style_value, $style_definition ) { * * @param array $style_value A single raw style value from the generate() $block_styles array. * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. - * @param boolean $should_skip_css_vars Whether to skip compiling CSS var values. + * @param array $block_styles Styles from a block's attributes object. + * @param array $options Options passed to the public generator functions. * * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). */ - protected static function get_css_declarations( $style_value, $style_definition, $should_skip_css_vars = false ) { + protected static function get_css_declarations( $style_value, $style_definition, $block_styles, $options ) { if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { - return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $should_skip_css_vars ); + return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $block_styles, $options ); } - $css_declarations = array(); - $style_property_keys = $style_definition['property_keys']; + $css_declarations = array(); + $style_property_keys = $style_definition['property_keys']; + $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; // Build CSS var values from var:? values, e.g, `var(--wp--css--rule-slug )` // Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition. @@ -370,6 +386,12 @@ protected static function get_css_declarations( $style_value, $style_definition, $value = static::get_css_var_value( $value, $style_definition['css_vars'] ); } $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); + + // If the style value contains a reference to another value in the tree. + if ( isset( $value['ref'] ) ) { + $value = static::get_ref_value( $block_styles, $value['ref'] ); + } + if ( static::is_valid_style_value( $style_value ) ) { $css_declarations[ $individual_property ] = $value; } @@ -387,7 +409,7 @@ protected static function get_css_declarations( $style_value, $style_definition, * * @param array $block_styles Styles from a block's attributes object. * @param array $options array( - * 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. + * 'selector' => (string) When a selector is passed, the style engine will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. * );. * @@ -401,9 +423,8 @@ public function get_block_supports_styles( $block_styles, $options ) { return null; } - $css_declarations = array(); - $classnames = array(); - $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; + $css_declarations = array(); + $classnames = array(); // Collect CSS and classnames. foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) { @@ -413,12 +434,17 @@ public function get_block_supports_styles( $block_styles, $options ) { foreach ( $definition_group_style as $style_definition ) { $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); + // If the style value contains a reference to another value in the tree. + if ( isset( $style_value['ref'] ) ) { + $style_value = static::get_ref_value( $block_styles, $style_value['ref'] ); + } + if ( ! static::is_valid_style_value( $style_value ) ) { continue; } $classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) ); - $css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $should_skip_css_vars ) ); + $css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $block_styles, $options ) ); } } @@ -448,6 +474,49 @@ public function get_block_supports_styles( $block_styles, $options ) { return $styles_output; } + /** + * Returns a stylesheet of CSS rules from a theme.json/global styles object. + * + * @param array $global_styles Styles object from theme.json. + * @param array $options array( + * 'selector' => (string) When a selector is passed, the style engine will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. + * );. + * + * @return string A stylesheet. + */ + public function generate_global_styles( $global_styles, $options ) { + if ( empty( $global_styles ) || ! is_array( $global_styles ) ) { + return null; + } + + // The return stylesheet. + $global_stylesheet = ''; + + // Layer 0: Root. + $root_level_options_defaults = array( + 'selector' => 'body', + ); + $root_level_options = wp_parse_args( $options, $root_level_options_defaults ); + $root_level_styles = $this->get_block_supports_styles( + $global_styles, + $root_level_options + ); + + if ( ! empty( $root_level_styles['css'] ) ) { + $global_stylesheet .= $root_level_styles['css'] . ' '; + } + + // @TODO Layer 1: Elements. + + // @TODO Layer 2: Blocks. + + if ( ! empty( $global_stylesheet ) ) { + return rtrim( $global_stylesheet ); + } + + return null; + } + /** * Style value parser that returns a CSS definition array comprising style properties * that have keys representing individual style properties, otherwise known as longhand CSS properties. @@ -455,19 +524,22 @@ public function get_block_supports_styles( $block_styles, $options ) { * "border-{top|right|bottom|left}-{color|width|style}: {value};" or, * "border-image-{outset|source|width|repeat|slice}: {value};" * - * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. - * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. - * @param boolean $should_skip_css_vars Whether to skip compiling CSS var values. + * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. + * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $block_styles Styles from a block's attributes object. + * @param array $options Options passed to the public generator functions. * * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). */ - protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $should_skip_css_vars ) { + protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $block_styles, $options ) { $css_declarations = array(); if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) { return $css_declarations; } + $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; + // The first item in $individual_property_definition['path'] array tells us the style property, e.g., "border". // We use this to get a corresponding CSS style definition such as "color" or "width" from the same group. // The second item in $individual_property_definition['path'] array refers to the individual property marker, e.g., "top". @@ -484,6 +556,11 @@ protected static function get_individual_property_css_declarations( $style_value $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition_path, null ); if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { + // If the style value contains a reference to another value in the tree. + if ( isset( $value['ref'] ) ) { + $value = static::get_ref_value( $block_styles, $value['ref'] ); + } + // Set a CSS var if there is a valid preset value. if ( is_string( $value ) && strpos( $value, 'var:' ) !== false && ! $should_skip_css_vars && ! empty( $individual_property_definition['css_vars'] ) ) { $value = static::get_css_var_value( $value, $individual_property_definition['css_vars'] ); @@ -522,3 +599,27 @@ function wp_style_engine_get_block_supports_styles( $block_styles, $options = ar } return null; } + +/** + * Global public interface method to WP_Style_Engine->generate_global_styles to generate a stylesheet styles from a single theme.json style object. + * See: https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/#styles + * + * Example usage: + * + * $styles = wp_style_engine_generate_global_styles( array( 'color' => array( 'text' => '#cccccc' ) ) ); + * // Returns `body { color: #cccccc }`. + * + * @access public + * + * @param array $global_styles The value of a block's attributes.style. + * @param array $options An array of options to determine the output. + * + * @return string A stylesheet. + */ +function wp_style_engine_generate_global_styles( $global_styles, $options = array() ) { + if ( class_exists( 'WP_Style_Engine' ) ) { + $style_engine = WP_Style_Engine::get_instance(); + return $style_engine->generate_global_styles( $global_styles, $options ); + } + return null; +} diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/class-wp-style-engine-test.php index 13334233ca445f..2cbe2ef62d449a 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -458,4 +458,129 @@ public function data_generate_block_supports_styles_fixtures() { ), ); } + + /** + * Tests generating styles and classnames based on various manifestations of the $global_styles argument. + * + * @dataProvider data_generate_global_styles_fixtures + */ + public function test_generate_global_styles( $global_styles, $options, $expected_output ) { + $generated_styles = wp_style_engine_generate_global_styles( $global_styles, $options ); + $this->assertSame( $expected_output, $generated_styles ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_generate_global_styles_fixtures() { + return array( + 'default_return_value' => array( + 'global_styles' => array(), + 'options' => null, + 'expected_output' => null, + ), + 'default_return_valid_top_level_css_rules' => array( + 'global_styles' => array( + 'color' => array( + 'text' => 'var(--wp--preset--color--foreground)', + 'background' => 'var(--wp--preset--color--background)', + ), + 'spacing' => array( + 'padding' => array( + 'top' => '10%', + 'right' => '20%', + 'bottom' => '10%', + 'left' => '20%', + ), + ), + 'typography' => array( + 'fontFamily' => 'var(--wp--preset--font-family--system-font)', + 'lineHeight' => 2, + 'fontSize' => '1rem', + ), + ), + 'options' => null, + 'expected_output' => 'body { color: var(--wp--preset--color--foreground); background-color: var(--wp--preset--color--background); padding-top: 10%; padding-right: 20%; padding-bottom: 10%; padding-left: 20%; font-size: 1rem; font-family: var(--wp--preset--font-family--system-font); line-height: 2; }', + ), + 'compiles_with_ref_pointers' => array( + 'global_styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.color.background' ), + ), + 'spacing' => array( + 'margin' => '2rem', + 'padding' => array( + 'top' => array( 'ref' => 'styles.spacing.margin' ), + 'right' => '20%', + 'bottom' => array( 'ref' => 'styles.spacing.margin' ), + 'left' => '20%', + ), + ), + 'border' => array( + 'top' => array( + 'width' => array( 'ref' => 'styles.spacing.padding.left' ), + 'style' => 'dashed', + ), + ), + ), + 'options' => null, + 'expected_output' => 'body { color: #ffffff; background-color: #ffffff; border-top-width: 20%; border-top-style: dashed; padding-top: 2rem; padding-right: 20%; padding-bottom: 2rem; padding-left: 20%; margin: 2rem; }', + ), + 'ignores_other_top_level_keys' => array( + 'global_styles' => array( + 'spacing' => array( + 'margin' => '20%', + ), + 'blocks' => array( + 'core/button' => array( + 'border' => array( + 'radius' => '0', + ), + 'color' => array( + 'text' => 'piano-red', + 'background' => 'muddy-waters', + ), + ), + 'core/site-title' => array( + 'typography' => array( + 'fontSize' => 'clamp(2em, 2vw, 4em)', + 'fontFamily' => 'Roboto,Oxygen-Sans,Ubuntu,sans-serif', + 'fontStyle' => 'italic', + ), + 'margin' => array( + 'bottom' => '20px', + ), + ), + ), + 'elements' => array( + 'h1' => array( + 'border' => array( + 'radius' => '0', + ), + 'color' => array( + 'text' => '#ffffff', + 'background' => '#000000', + ), + ), + 'link' => array( + 'typography' => array( + 'fontStyle' => 'underline', + ), + 'margin' => array( + 'bottom' => '20px', + ), + 'color' => array( + 'text' => 'var(--wp--preset--color--foreground)', + ), + ), + ), + ), + 'options' => null, + 'expected_output' => 'body { margin: 20%; }', + ), + ); + } } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 0c830d8de9a9c9..e98d820fb9c154 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -526,7 +526,7 @@ function test_get_property_value_valid() { ) ); - $expected = 'body { margin: 0; }body{background-color: #ffffff;color: #000000;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{background-color: #000000;color: #ffffff;}'; + $expected = 'body { margin: 0; }body { color: #000000; background-color: #ffffff; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{background-color: #000000;color: #ffffff;}'; $this->assertEquals( $expected, $theme_json->get_stylesheet() ); } @@ -558,7 +558,7 @@ function test_get_property_value_loop() { ) ); - $expected = 'body { margin: 0; }body{background-color: #ffffff;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $expected = 'body { margin: 0; }body { background-color: #ffffff; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; $this->assertSame( $expected, $theme_json->get_stylesheet() ); } @@ -589,7 +589,7 @@ function test_get_property_value_recursion() { ) ); - $expected = 'body { margin: 0; }body{background-color: #ffffff;color: #ffffff;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $expected = 'body { margin: 0; }body { color: #ffffff; background-color: #ffffff; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; $this->assertEquals( $expected, $theme_json->get_stylesheet() ); } @@ -612,7 +612,7 @@ function test_get_property_value_self() { ) ); - $expected = 'body { margin: 0; }body{background-color: #ffffff;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + $expected = 'body { margin: 0; }body { background-color: #ffffff; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; $this->assertEquals( $expected, $theme_json->get_stylesheet() ); }