diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index 62671c5f81d01d..9230fd48b1c102 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -87,16 +87,8 @@ function gutenberg_render_elements_support( $block_content, $block ) { * @return null */ function gutenberg_render_elements_support_styles( $pre_render, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $skip_link_color_serialization = gutenberg_should_skip_block_supports_serialization( $block_type, 'color', 'link' ); - if ( $skip_link_color_serialization ) { - return null; - } - - $link_color = null; - if ( ! empty( $block['attrs'] ) ) { - $link_color = _wp_array_get( $block['attrs'], array( 'style', 'elements', 'link', 'color', 'text' ), null ); - } + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $element_block_styles = isset( $block['attrs']['style']['elements'] ) ? $block['attrs']['style']['elements'] : null; /* * For now we only care about link color. @@ -104,23 +96,27 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { * should take advantage of WP_Theme_JSON_Gutenberg::compute_style_properties * and work for any element and style. */ - if ( null === $link_color ) { + $skip_link_color_serialization = gutenberg_should_skip_block_supports_serialization( $block_type, 'color', 'link' ); + + if ( $skip_link_color_serialization ) { return null; } + $class_name = gutenberg_get_elements_class_name( $block ); + $link_block_styles = isset( $element_block_styles['link'] ) ? $element_block_styles['link'] : null; + + if ( $link_block_styles ) { + $styles = gutenberg_style_engine_generate( + $link_block_styles, + array( + 'selector' => ".$class_name a", + 'css_vars' => true, + ) + ); - $class_name = gutenberg_get_elements_class_name( $block ); - - if ( strpos( $link_color, 'var:preset|color|' ) !== false ) { - // Get the name from the string and add proper styles. - $index_to_splice = strrpos( $link_color, '|' ) + 1; - $link_color_name = substr( $link_color, $index_to_splice ); - $link_color = "var(--wp--preset--color--$link_color_name)"; + if ( ! empty( $styles['css'] ) ) { + gutenberg_enqueue_block_support_styles( $styles['css'] ); + } } - $link_color_declaration = esc_html( safecss_filter_attr( "color: $link_color" ) ); - - $style = ".$class_name a{" . $link_color_declaration . ';}'; - - gutenberg_enqueue_block_support_styles( $style ); return null; } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index 5687a2f422186e..c14b0e731e8646 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -35,8 +35,10 @@ class WP_Style_Engine { * parse/output valid Gutenberg styles from a block's attributes. * For every style definition, the follow properties are valid: * - classnames => an array of classnames to be returned for block styles. The key is a classname or pattern. - * A value of `true` means the classname should be applied always. Otherwise a valid CSS property - * to match the incoming value, e.g., "color" to match var:preset|color|somePresetName. + * A value of `true` means the classname should be applied always. Otherwise, a valid CSS property (string) + * to match the incoming value, e.g., "color" to match var:preset|color|somePresetSlug. + * - css_vars => an array of key value pairs used to generate CSS var values. The key is a CSS var pattern, whose `$slug` fragment will be replaced with a preset slug. + * The value should be a valid CSS property (string) to match the incoming value, e.g., "color" to match var:preset|color|somePresetSlug. * - property_key => the key that represents a valid CSS property, e.g., "margin" or "border". * - path => a path that accesses the corresponding style value in the block style object. */ @@ -45,6 +47,9 @@ class WP_Style_Engine { 'text' => array( 'property_key' => 'color', 'path' => array( 'color', 'text' ), + 'css_vars' => array( + '--wp--preset--color--$slug' => 'color', + ), 'classnames' => array( 'has-text-color' => true, 'has-%s-color' => 'color', @@ -137,8 +142,8 @@ public static function get_instance() { /** * Extracts the slug in kebab case from a preset string, e.g., "heavenly-blue" from 'var:preset|color|heavenlyBlue'. * - * @param string $style_value A single css preset value. - * @param string $property_key The CSS property that is the second element of the preset string. Used for matching. + * @param string? $style_value A single css preset value. + * @param string $property_key The CSS property that is the second element of the preset string. Used for matching. * * @return string|null The slug, or null if not found. */ @@ -186,17 +191,49 @@ 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_return_css_vars Whether to try to build and return CSS var values. * * @return array An array of CSS rules. */ - protected static function get_css( $style_value, $style_definition ) { - // Low-specificity check to see if the value is a CSS preset. + protected static function get_css( $style_value, $style_definition, $should_return_css_vars ) { + $rules = array(); + + if ( ! $style_value ) { + return $rules; + } + + $style_property = $style_definition['property_key']; + + // 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. if ( is_string( $style_value ) && strpos( $style_value, 'var:' ) !== false ) { - return array(); + if ( $should_return_css_vars && ! empty( $style_definition['css_vars'] ) ) { + foreach ( $style_definition['css_vars'] as $css_var_pattern => $property_key ) { + $slug = static::get_slug_from_preset_value( $style_value, $property_key ); + if ( $slug ) { + $css_var = strtr( + $css_var_pattern, + array( '$slug' => $slug ) + ); + $rules[ $style_property ] = "var($css_var)"; + } + } + } + return $rules; + } + + // Default rule builder. + // If the input contains an array, assume box model-like properties + // for styles such as margins and padding. + if ( is_array( $style_value ) ) { + foreach ( $style_value as $key => $value ) { + $rules[ "$style_property-$key" ] = $value; + } + } else { + $rules[ $style_property ] = $style_value; } - // If required in the future, style definitions could define a callable `value_func` to generate custom CSS rules. - return static::get_css_rules( $style_value, $style_definition['property_key'] ); + return $rules; } /** @@ -204,23 +241,31 @@ protected static function get_css( $style_value, $style_definition ) { * Styles are bundled based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. * * @param array $block_styles An array of styles from a block's attributes. + * @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. + * 'css_vars' => (boolean) Whether to covert CSS values to var() values. If `true` the style engine will try to parse var:? values and output var( --wp--preset--* ) rules. Default is `false`. + * );. * * @return array|null array( - * 'styles' => (string) A CSS ruleset formatted to be placed in an HTML `style` attribute or tag. + * 'css' => (string) A CSS ruleset formatted to be placed in an HTML `style` attribute or tag. Default is a string of inline styles. * 'classnames' => (string) Classnames separated by a space. * ); */ - public function generate( $block_styles ) { + public function generate( $block_styles, $options ) { if ( empty( $block_styles ) || ! is_array( $block_styles ) ) { return null; } - $css_rules = array(); - $classnames = array(); - $styles_output = array(); + $css_rules = array(); + $classnames = array(); + $should_return_css_vars = isset( $options['css_vars'] ) && true === $options['css_vars']; // Collect CSS and classnames. foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) { + if ( ! $definition_group ) { + continue; + } + foreach ( $definition_group as $style_definition ) { $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); @@ -229,63 +274,45 @@ public function generate( $block_styles ) { } $classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) ); - $css_rules = array_merge( $css_rules, static::get_css( $style_value, $style_definition ) ); + $css_rules = array_merge( $css_rules, static::get_css( $style_value, $style_definition, $should_return_css_vars ) ); } } // Build CSS rules output. - $css_output = ''; + $selector = isset( $options['selector'] ) ? $options['selector'] : null; + $css = array(); + $styles_output = array(); + if ( ! empty( $css_rules ) ) { // Generate inline style rules. - // In the future there might be a flag in the option to output - // inline CSS rules (for HTML style attributes) vs selectors + rules for style tags. foreach ( $css_rules as $rule => $value ) { $filtered_css = esc_html( safecss_filter_attr( "{$rule}: {$value}" ) ); if ( ! empty( $filtered_css ) ) { - $css_output .= $filtered_css . '; '; + $css[] = $filtered_css . ';'; } } } - if ( ! empty( $css_output ) ) { - $styles_output['css'] = trim( $css_output ); + // Return css, if any. + if ( ! empty( $css ) ) { + // Return an entire rule if there is a selector. + if ( $selector ) { + $style_block = "$selector { "; + $style_block .= implode( ' ', $css ); + $style_block .= ' }'; + $styles_output['css'] = $style_block; + } else { + $styles_output['css'] = implode( ' ', $css ); + } } + // Return classnames, if any. if ( ! empty( $classnames ) ) { $styles_output['classnames'] = implode( ' ', array_unique( $classnames ) ); } return $styles_output; } - - /** - * Default style value parser that returns a CSS ruleset. - * If the input contains an array, it will be treated like a box model - * for styles such as margins and padding - * - * @param string|array $style_value A single raw Gutenberg style attributes value for a CSS property. - * @param string $style_property The CSS property for which we're creating a rule. - * - * @return array The class name for the added style. - */ - protected static function get_css_rules( $style_value, $style_property ) { - $rules = array(); - - if ( ! $style_value ) { - return $rules; - } - - // We assume box model-like properties. - if ( is_array( $style_value ) ) { - foreach ( $style_value as $key => $value ) { - $rules[ "$style_property-$key" ] = $value; - } - } else { - $rules[ $style_property ] = $style_value; - } - - return $rules; - } } /** @@ -294,17 +321,20 @@ protected static function get_css_rules( $style_value, $style_property ) { * Returns an CSS ruleset. * Styles are bundled based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. * + * @access public + * * @param array $block_styles An array of styles from a block's attributes. + * @param array $options An array of options to determine the output. * * @return array|null array( * 'styles' => (string) A CSS ruleset formatted to be placed in an HTML `style` attribute or tag. * 'classnames' => (string) Classnames separated by a space. * ); */ -function wp_style_engine_generate( $block_styles ) { +function wp_style_engine_generate( $block_styles, $options = array() ) { if ( class_exists( 'WP_Style_Engine' ) ) { $style_engine = WP_Style_Engine::get_instance(); - return $style_engine->generate( $block_styles ); + return $style_engine->generate( $block_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 f9830e173e11a7..d6c83f85394023 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -17,8 +17,8 @@ class WP_Style_Engine_Test extends WP_UnitTestCase { * * @dataProvider data_generate_styles_fixtures */ - function test_generate_styles( $block_styles, $expected_output ) { - $generated_styles = wp_style_engine_generate( $block_styles ); + function test_generate_styles( $block_styles, $options, $expected_output ) { + $generated_styles = wp_style_engine_generate( $block_styles, $options ); $this->assertSame( $expected_output, $generated_styles ); } @@ -31,11 +31,13 @@ public function data_generate_styles_fixtures() { return array( 'default_return_value' => array( 'block_styles' => array(), + 'options' => null, 'expected_output' => null, ), 'inline_invalid_block_styles_empty' => array( 'block_styles' => 'hello world!', + 'options' => null, 'expected_output' => null, ), @@ -43,6 +45,7 @@ public function data_generate_styles_fixtures() { 'block_styles' => array( 'pageBreakAfter' => 'verso', ), + 'options' => null, 'expected_output' => array(), ), @@ -50,6 +53,7 @@ public function data_generate_styles_fixtures() { 'block_styles' => array( 'pageBreakAfter' => 'verso', ), + 'options' => null, 'expected_output' => array(), ), @@ -59,6 +63,7 @@ public function data_generate_styles_fixtures() { 'gap' => '1000vw', ), ), + 'options' => null, 'expected_output' => array(), ), @@ -71,6 +76,7 @@ public function data_generate_styles_fixtures() { 'margin' => '111px', ), ), + 'options' => array(), 'expected_output' => array( 'css' => 'margin: 111px;', 'classnames' => 'has-text-color has-texas-flood-color', @@ -94,6 +100,7 @@ public function data_generate_styles_fixtures() { ), ), ), + 'options' => null, 'expected_output' => array( 'css' => 'padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; margin-top: 12rem; margin-left: 2vh; margin-bottom: 2px; margin-right: 10em;', ), @@ -112,10 +119,57 @@ public function data_generate_styles_fixtures() { 'letterSpacing' => '2', ), ), + 'options' => null, 'expected_output' => array( 'css' => 'font-family: Roboto,Oxygen-Sans,Ubuntu,sans-serif; font-style: italic; font-weight: 800; line-height: 1.3; text-decoration: underline; text-transform: uppercase; letter-spacing: 2;', ), ), + + 'style_block_with_selector' => array( + 'block_styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ), + 'options' => array( 'selector' => '.wp-selector > p' ), + 'expected_output' => array( + 'css' => '.wp-selector > p { padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; }', + ), + ), + + 'elements_with_css_var_value' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|my-little-pony', + ), + ), + 'options' => array( + 'selector' => '.wp-selector', + 'css_vars' => true, + ), + 'expected_output' => array( + 'css' => '.wp-selector { color: var(--wp--preset--color--my-little-pony); }', + 'classnames' => 'has-text-color has-my-little-pony-color', + ), + ), + + 'elements_with_invalid_preset_style_property' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|invalid_property|my-little-pony', + ), + ), + 'options' => array( 'selector' => '.wp-selector' ), + 'expected_output' => array( + 'classnames' => 'has-text-color', + ), + ), + 'valid_classnames_deduped' => array( 'block_styles' => array( 'color' => array( @@ -128,10 +182,25 @@ public function data_generate_styles_fixtures() { 'fontFamily' => 'var:preset|font-family|totally-awesome', ), ), + 'options' => array(), 'expected_output' => array( 'classnames' => 'has-text-color has-copper-socks-color has-background has-splendid-carrot-background-color has-like-wow-dude-gradient-background has-fantastic-font-size has-totally-awesome-font-family', ), ), + + 'valid_classnames_and_css_vars' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|teal-independents', + ), + ), + 'options' => array( 'css_vars' => true ), + 'expected_output' => array( + 'css' => 'color: var(--wp--preset--color--teal-independents);', + 'classnames' => 'has-text-color has-teal-independents-color', + ), + ), + 'valid_classnames_with_null_style_values' => array( 'block_styles' => array( 'color' => array( @@ -139,11 +208,13 @@ public function data_generate_styles_fixtures() { 'background' => null, ), ), + 'options' => array(), 'expected_output' => array( 'css' => 'color: #fff;', 'classnames' => 'has-text-color', ), ), + 'invalid_classnames_preset_value' => array( 'block_styles' => array( 'color' => array( @@ -155,10 +226,12 @@ public function data_generate_styles_fixtures() { 'padding' => 'var:preset|spacing|padding', ), ), + 'options' => array(), 'expected_output' => array( 'classnames' => 'has-text-color has-background', ), ), + 'invalid_classnames_options' => array( 'block_styles' => array( 'typography' => array( @@ -170,6 +243,7 @@ public function data_generate_styles_fixtures() { ), ), ), + 'options' => array(), 'expected_output' => array(), ), );