diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 81499d15e5905..2ccfdefa95e0b 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -28,6 +28,14 @@ * @since 6.7.0 Added support for typography.writingMode. * @since 7.0.0 Added support for typography.textIndent. * @since 7.2.0 Added support for typography.textShadow. + * + * @phpstan-type Style_Definition array{ + * property_keys?: array{ default: string, individual?: string }, + * path: non-empty-list, + * css_vars?: array, + * classnames?: array, + * value_func?: callable( mixed, array, array ): array, + * } */ #[AllowDynamicProperties] final class WP_Style_Engine { @@ -59,7 +67,8 @@ final class WP_Style_Engine { * @since 6.1.0 * @since 7.1.0 Added `background.gradient` property. * @since 7.2.0 Added `background.backgroundClip` property. - * @var array + * @var array> + * @phpstan-var array> */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( 'background' => array( @@ -403,7 +412,7 @@ final class WP_Style_Engine { * * @since 6.1.0 * - * @param string $style_value A single CSS preset value. + * @param mixed $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 The slug, or empty string if not found. @@ -448,7 +457,7 @@ protected static function get_css_var_value( $style_value, $css_vars ) { * * @since 6.1.0 * - * @param string $style_value A single CSS preset value. + * @param mixed $style_value A single raw style value. * @return bool */ protected static function is_valid_style_value( $style_value ) { @@ -473,11 +482,21 @@ protected static function is_valid_style_value( $style_value ) { * or a CSS nested @rule, such as `@media (min-width: 80rem)` * or `@layer module`. */ - public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ): void { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations ); + + $store = static::get_store( $store_name ); + if ( ! $store ) { + return; + } + + // The rule is null when the selector is only whitespace. + $rule = $store->add_rule( $css_selector, $rules_group ); + if ( $rule ) { + $rule->add_declarations( $css_declarations ); + } } /** @@ -495,12 +514,12 @@ public static function get_store( $store_name ) { /** * Returns classnames and CSS based on the values in a styles object. * - * Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. + * Return values are parsed based on the instructions in {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * * @since 6.1.0 * - * @param array $block_styles The style object. - * @param array $options { + * @param array $block_styles The style object. + * @param array $options { * Optional. An array of options. Default empty array. * * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, @@ -567,10 +586,13 @@ public static function parse_block_styles( $block_styles, $options ) { * * @since 6.1.0 * - * @param string $style_value A single raw style value or CSS preset property + * @param mixed $style_value A single raw style value or CSS preset property * from the `$block_styles` array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An array of CSS classnames, or empty array if there are none. + * + * @phpstan-param Style_Definition $style_definition + * @phpstan-return list */ protected static function get_classnames( $style_value, $style_definition ) { if ( empty( $style_value ) ) { @@ -589,7 +611,7 @@ protected static function get_classnames( $style_value, $style_definition ) { if ( $slug ) { /* - * Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA. + * Right now we expect a classname pattern to be stored in self::BLOCK_STYLE_DEFINITIONS_METADATA. * One day, if there are no stored schemata, we could allow custom patterns or * generate classnames based on other properties * such as a path or a value or a prefix passed in options. @@ -608,7 +630,7 @@ protected static function get_classnames( $style_value, $style_definition ) { * @since 6.1.0 * * @param mixed $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @param array $options { * Optional. An array of options. Default empty array. * @@ -617,12 +639,18 @@ protected static function get_classnames( $style_value, $style_definition ) { * to `var( --wp--preset--* )` values. Default false. * } * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`. + * + * @phpstan-param Style_Definition $style_definition */ protected static function get_css_declarations( $style_value, $style_definition, $options = array() ) { if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options ); } + if ( ! isset( $style_definition['property_keys'] ) ) { + return array(); + } + $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']; @@ -659,7 +687,7 @@ protected static function get_css_declarations( $style_value, $style_definition, $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); - if ( $individual_property && static::is_valid_style_value( $value ) ) { + if ( $individual_property && is_string( $value ) && static::is_valid_style_value( $value ) ) { $css_declarations[ $individual_property ] = $value; } } @@ -667,7 +695,10 @@ protected static function get_css_declarations( $style_value, $style_definition, return $css_declarations; } - $css_declarations[ $style_property_keys['default'] ] = $style_value; + // CSS declarations only accept string values. + if ( is_string( $style_value ) ) { + $css_declarations[ $style_property_keys['default'] ] = $style_value; + } return $css_declarations; } @@ -689,8 +720,8 @@ protected static function get_css_declarations( $style_value, $style_definition, * * @since 6.1.0 * - * @param array $style_value A single raw style value from `$block_styles` array. - * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA + * @param mixed $style_value A single raw style value from `$block_styles` array. + * @param array $individual_property_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA} * representing an individual property of a CSS property, * e.g. 'top' in 'border-top'. * @param array $options { @@ -701,6 +732,8 @@ protected static function get_css_declarations( $style_value, $style_definition, * to `var( --wp--preset--* )` values. Default false. * } * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`. + * + * @phpstan-param Style_Definition $individual_property_definition */ protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options = array() ) { if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) { @@ -721,17 +754,17 @@ protected static function get_individual_property_css_declarations( $style_value $css_declarations = array(); foreach ( $style_value as $css_property => $value ) { - if ( empty( $value ) ) { + // CSS declarations only accept string values. + if ( empty( $value ) || ! is_string( $value ) ) { continue; } - // Build a path to the individual rules in definitions. - $style_definition_path = array( $definition_group_key, $css_property ); - $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition_path, null ); + // Get the corresponding style definition from the same group, e.g. `border.color`. + $style_definition = static::BLOCK_STYLE_DEFINITIONS_METADATA[ $definition_group_key ][ $css_property ] ?? null; if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { // Set a CSS var if there is a valid preset value. - if ( is_string( $value ) && str_contains( $value, 'var:' ) + if ( str_contains( $value, 'var:' ) && ! $should_skip_css_vars && ! empty( $individual_property_definition['css_vars'] ) ) { $value = static::get_css_var_value( $value, $individual_property_definition['css_vars'] ); @@ -753,9 +786,12 @@ protected static function get_individual_property_css_declarations( $style_value * * @since 6.4.0 * - * @param array $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param mixed $style_value A single raw style value from $block_styles array. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * + * @phpstan-param Style_Definition $style_definition + * @phpstan-return array */ protected static function get_url_or_value_css_declaration( $style_value, $style_definition ) { if ( empty( $style_value ) ) { @@ -767,7 +803,7 @@ protected static function get_url_or_value_css_declaration( $style_value, $style if ( isset( $style_definition['property_keys']['default'] ) ) { $value = null; - if ( ! empty( $style_value['url'] ) ) { + if ( is_array( $style_value ) && ! empty( $style_value['url'] ) && is_string( $style_value['url'] ) ) { $value = "url('" . $style_value['url'] . "')"; } elseif ( is_string( $style_value ) ) { $value = $style_value; @@ -789,10 +825,11 @@ protected static function get_url_or_value_css_declaration( $style_value, $style * * @since 7.2.0 * - * @param string $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param mixed $style_value A single raw style value from $block_styles array. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return array An associative array of CSS definitions, mapping properties to values. * + * @phpstan-param Style_Definition $style_definition * @phpstan-return array */ protected static function get_background_clip_css_declarations( $style_value, $style_definition ) { diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index af6444d9bf88d..3cd23ef57d807 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -995,4 +995,60 @@ public function test_should_return_stylesheet_with_nested_rules() { $this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); } + + /** + * Tests that storing a rule whose selector is only whitespace is skipped + * instead of calling {@see WP_Style_Engine_CSS_Rule::add_declarations()} on null. + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + * @covers WP_Style_Engine::store_css_rule + */ + public function test_should_skip_storing_rule_with_whitespace_selector() { + $css_rules = array( + array( + 'selector' => ' ', + 'declarations' => array( + 'color' => 'red', + ), + ), + ); + + wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'context' => 'test-whitespace-selector' ) ); + + $store = WP_Style_Engine::get_store( 'test-whitespace-selector' ); + $this->assertInstanceOf( WP_Style_Engine_CSS_Rules_Store::class, $store ); + $this->assertSame( array(), $store->get_all_rules(), 'Expected no rule to be stored.' ); + } + + /** + * Tests that non-string style values are left out of the returned declarations, + * since {@see WP_Style_Engine_CSS_Declarations::add_declaration()} would drop them from the CSS anyway. + * + * @covers ::wp_style_engine_get_styles + */ + public function test_should_omit_non_string_values_from_declarations() { + $block_styles = array( + 'typography' => array( + 'textColumns' => 2, + 'fontSize' => '12px', + ), + 'spacing' => array( + 'padding' => array( + 'top' => 5, + 'left' => '1em', + ), + ), + ); + + $this->assertSame( + array( + 'css' => 'padding-left:1em;font-size:12px;', + 'declarations' => array( + 'padding-left' => '1em', + 'font-size' => '12px', + ), + ), + wp_style_engine_get_styles( $block_styles ) + ); + } }