diff --git a/lib/load.php b/lib/load.php index 47d41fb50b3b5f..736a26f31d9ec9 100644 --- a/lib/load.php +++ b/lib/load.php @@ -197,6 +197,7 @@ function gutenberg_is_experiment_enabled( $name ) { if ( is_dir( __DIR__ . '/../build/style-engine' ) ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-declarations-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rule-gutenberg.php'; + require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-container-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-store-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-processor-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php'; diff --git a/packages/style-engine/class-wp-style-engine-css-rule.php b/packages/style-engine/class-wp-style-engine-css-rule.php index 18d8c2f7ef7f0f..92410009c2338f 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -10,7 +10,7 @@ if ( ! class_exists( 'WP_Style_Engine_CSS_Rule' ) ) { /** - * Holds, sanitizes, processes and prints CSS declarations for the Style Engine. + * Holds, sanitizes, processes and prints CSS rules for the Style Engine. * * @access private */ @@ -32,27 +32,16 @@ class WP_Style_Engine_CSS_Rule { */ protected $declarations; - /** - * The CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. - * - * @var string - */ - protected $at_rule; - - /** * Constructor * * @param string $selector The CSS selector. * @param string[]|WP_Style_Engine_CSS_Declarations $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ), * or a WP_Style_Engine_CSS_Declarations object. - * @param string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. - * */ - public function __construct( $selector = '', $declarations = array(), $at_rule = '' ) { + public function __construct( $selector = '', $declarations = array() ) { $this->set_selector( $selector ); $this->add_declarations( $declarations ); - $this->set_at_rule( $at_rule ); } /** @@ -63,7 +52,9 @@ public function __construct( $selector = '', $declarations = array(), $at_rule = * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. */ public function set_selector( $selector ) { - $this->selector = $selector; + if ( ! empty( $selector ) ) { + $this->selector = $selector; + } return $this; } @@ -76,6 +67,9 @@ public function set_selector( $selector ) { * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. */ public function add_declarations( $declarations ) { + if ( empty( $declarations ) ) { + return $this; + } $is_declarations_object = ! is_array( $declarations ); $declarations_array = $is_declarations_object ? $declarations->get_declarations() : $declarations; @@ -91,18 +85,6 @@ public function add_declarations( $declarations ) { return $this; } - /** - * Sets the at_rule. - * - * @param string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. - * - * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. - */ - public function set_at_rule( $at_rule ) { - $this->at_rule = $at_rule; - return $this; - } - /** * Gets the declarations object. * @@ -121,15 +103,6 @@ public function get_selector() { return $this->selector; } - /** - * Gets the at_rule. - * - * @return string - */ - public function get_at_rule() { - return $this->at_rule; - } - /** * Gets the CSS. * @@ -139,28 +112,19 @@ public function get_at_rule() { * @return string */ public function get_css( $should_prettify = false, $indent_count = 0 ) { - $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; - $nested_rule_indent = $should_prettify ? str_repeat( "\t", $indent_count + 1 ) : ''; - $declarations_indent = $should_prettify ? $indent_count + 1 : 0; - $nested_declarations_indent = $should_prettify ? $indent_count + 2 : 0; - $suffix = $should_prettify ? "\n" : ''; - $spacer = $should_prettify ? ' ' : ''; + $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; + $declarations_indent = $should_prettify ? $indent_count + 1 : 0; + $suffix = $should_prettify ? "\n" : ''; + $spacer = $should_prettify ? ' ' : ''; // Trims any multiple selectors strings. $selector = $should_prettify ? implode( ',', array_map( 'trim', explode( ',', $this->get_selector() ) ) ) : $this->get_selector(); $selector = $should_prettify ? str_replace( array( ',' ), ",\n", $selector ) : $selector; - $at_rule = $this->get_at_rule(); - $has_at_rule = ! empty( $at_rule ); - $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_at_rule ? $nested_declarations_indent : $declarations_indent ); + $css_declarations = ! empty( $this->declarations ) ? $this->declarations->get_declarations_string( $should_prettify, $declarations_indent ) : ''; if ( empty( $css_declarations ) ) { return ''; } - if ( $has_at_rule ) { - $selector = "{$rule_indent}{$at_rule}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}"; - return $selector; - } - return "{$rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$rule_indent}}"; } } diff --git a/packages/style-engine/class-wp-style-engine-css-rules-container.php b/packages/style-engine/class-wp-style-engine-css-rules-container.php new file mode 100644 index 00000000000000..c38f377bc09040 --- /dev/null +++ b/packages/style-engine/class-wp-style-engine-css-rules-container.php @@ -0,0 +1,123 @@ +set_selector( $selector ); + $this->add_rules( $rule ); + } + + /** + * Gets all nested rules. + * + * @return WP_Style_Engine_CSS_Rule[] + */ + public function get_rules() { + return $this->rules; + } + + /** + * Gets a stored nested rules. + * + * @return WP_Style_Engine_CSS_Rule + */ + public function get_rule( $selector ) { + return $this->rules[ $selector ] ?? null; + } + + /** + * Adds the rules. + * + * @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[] $container_rules An array of declarations (property => value pairs), + * or a WP_Style_Engine_CSS_Declarations object. + * + * @return WP_Style_Engine_CSS_Rules_Container Returns the object to allow chaining of methods. + */ + public function add_rules( $rules ) { + if ( empty( $rules ) ) { + return $this; + } + + if ( ! is_array( $rules ) ) { + $rules = array( $rules ); + } + + foreach ( $rules as $rule ) { + if ( ! $rule instanceof WP_Style_Engine_CSS_Rule ) { + _doing_it_wrong( + __METHOD__, + __( 'Rules passed to WP_Style_Engine_CSS_Rules_Container must be an instance of WP_Style_Engine_CSS_Rule', 'default' ), + '6.6.0' + ); + continue; + } + + $selector = $rule->get_selector(); + + if ( isset( $this->rules[ $selector ] ) ) { + $this->rules[ $selector ]->add_declarations( $rule->get_declarations() ); + } else { + $this->rules[ $selector ] = $rule; + } + } + + return $this; + } + + /** + * Gets the nested CSS. + * + * @param bool $should_prettify Whether to add spacing, new lines and indents. + * @param number $indent_count The number of tab indents to apply to the rule. Applies if `prettify` is `true`. + * + * @return string + */ + public function get_css( $should_prettify = false, $indent_count = 0 ) { + $css = ''; + $indent_count = $should_prettify ? $indent_count + 1 : $indent_count; + $new_line = $should_prettify ? "\n" : ''; + $spacer = $should_prettify ? ' ' : ''; + $css .= ! empty( $this->declarations ) ? $this->declarations->get_declarations_string( $should_prettify, $indent_count ) : ''; + $css .= $should_prettify && $css ? "\n" : ''; + + foreach ( $this->rules as $rule ) { + $css .= $rule->get_css( $should_prettify, $indent_count ); + $css .= $should_prettify ? "\n" : ''; + } + + if ( empty( $css ) ) { + return $css; + } + + return "{$this->selector}{$spacer}{{$new_line}{$css}}"; + } + } +} diff --git a/packages/style-engine/class-wp-style-engine-css-rules-store.php b/packages/style-engine/class-wp-style-engine-css-rules-store.php index e199cda5da0fcb..04c3ccab89d966 100644 --- a/packages/style-engine/class-wp-style-engine-css-rules-store.php +++ b/packages/style-engine/class-wp-style-engine-css-rules-store.php @@ -109,30 +109,35 @@ public function get_all_rules() { * Gets a WP_Style_Engine_CSS_Rule object by its selector. * If the rule does not exist, it will be created. * - * @param string $selector The CSS selector. - * @param string $at_rule The CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @param string|WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rules_Container $rule The CSS selector or a WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rules_Container object. * * @return WP_Style_Engine_CSS_Rule|void Returns a WP_Style_Engine_CSS_Rule object, or null if the selector is empty. */ - public function add_rule( $selector, $at_rule = '' ) { - $selector = trim( $selector ); - $at_rule = trim( $at_rule ); - - // Bail early if there is no selector. - if ( empty( $selector ) ) { + public function add_rule( $rule ) { + if ( empty( $rule ) ) { return; } - if ( ! empty( $at_rule ) ) { - if ( empty( $this->rules[ "$at_rule $selector" ] ) ) { - $this->rules[ "$at_rule $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $at_rule ); + if ( is_string( $rule ) ) { + $selector = trim( $rule ); + /* + Create a new WP_Style_Engine_CSS_Rule rule by default if it doesn't exist. + */ + if ( ! isset( $this->rules[ $selector ] ) ) { + $rule = new WP_Style_Engine_CSS_Rule( $selector ); + } else { + return $this->rules[ $selector ]; } - return $this->rules[ "$at_rule $selector" ]; } - // Create the rule if it doesn't exist. - if ( empty( $this->rules[ $selector ] ) ) { - $this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector ); + $selector = $rule->get_selector(); + if ( isset( $this->rules[ $selector ] ) ) { + if ( $rule instanceof WP_Style_Engine_CSS_Rules_Container && $this->rules[ $selector ] instanceof WP_Style_Engine_CSS_Rules_Container ) { + $this->rules[ $selector ]->add_rules( $rule->get_rules() ); + } + $this->rules[ $selector ]->add_declarations( $rule->get_declarations() ); + } else { + $this->rules[ $selector ] = $rule; } return $this->rules[ $selector ]; diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index 3f8cef0a2cf315..3d2aaf81b21439 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -30,6 +30,14 @@ class WP_Style_Engine_Processor { */ protected $css_rules = array(); + /** + * The set of nested CSS rules that this processor will work on. + * + * @var WP_Style_Engine_CSS_Rules_Container[] + */ + protected $css_containers = array(); + + /** * Add a store to the processor. * @@ -55,7 +63,7 @@ public function add_store( $store ) { /** * Adds rules to be processed. * - * @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[] $css_rules A single, or an array of, WP_Style_Engine_CSS_Rule objects from a store or otherwise. + * @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[]|WP_Style_Engine_CSS_Rules_Container|WP_Style_Engine_CSS_Rules_Container[] $css_rules A single, or an array of, WP_Style_Engine_CSS_Rule objects from a store or otherwise. * * @return WP_Style_Engine_Processor Returns the object to allow chaining methods. */ @@ -65,30 +73,37 @@ public function add_rules( $css_rules ) { } foreach ( $css_rules as $rule ) { + // Check for rules that need to be nested in containers. $selector = $rule->get_selector(); - $at_rule = $rule->get_at_rule(); - /** - * If there is an at_rule and it already exists in the css_rules array, - * add the rule to it. - * Otherwise, create a new entry for the at_rule + if ( empty( $selector ) ) { + continue; + } + + /* + * Merge existing rule and container objects or create new ones. + * Containers and rules are stored in separate arrays to allow for + * separate processing. */ - if ( ! empty( $at_rule ) ) { - if ( isset( $this->css_rules[ "$at_rule $selector" ] ) ) { - $this->css_rules[ "$at_rule $selector" ]->add_declarations( $rule->get_declarations() ); - continue; + if ( $rule instanceof WP_Style_Engine_CSS_Rules_Container ) { + if ( isset( $this->css_containers[ $selector ] ) ) { + $this->css_containers[ $selector ]->add_rules( $rule->get_rules() ); + $this->css_containers[ $selector ]->add_declarations( $rule->get_declarations() ); + } else { + $this->css_containers[ $selector ] = $rule; } - $this->css_rules[ "$at_rule $selector" ] = $rule; continue; } - // If the selector already exists, add the declarations to it. - if ( isset( $this->css_rules[ $selector ] ) ) { - $this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() ); - continue; + if ( $rule instanceof WP_Style_Engine_CSS_Rule ) { + if ( isset( $this->css_rules[ $selector ] ) ) { + $this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() ); + } else { + $this->css_rules[ $selector ] = $rule; + } } - $this->css_rules[ $rule->get_selector() ] = $rule; } + return $this; } @@ -125,11 +140,14 @@ public function get_css( $options = array() ) { // Build the CSS. $css = ''; - foreach ( $this->css_rules as $rule ) { - // See class WP_Style_Engine_CSS_Rule for the get_css method. + // Merge the rules and containers. Containers come last. + $merged_rules = array_merge( $this->css_rules, $this->css_containers ); + + foreach ( $merged_rules as $rule ) { $css .= $rule->get_css( $options['prettify'] ); $css .= $options['prettify'] ? "\n" : ''; } + return $css; } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index a97e7cb8a0b213..b0c4b7f0002424 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -358,11 +358,11 @@ protected static function is_valid_style_value( $style_value ) { * * @return void. */ - public static function store_css_rule( $store_name, $css_selector, $css_declarations, $css_at_rule = '' ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations ) { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector, $css_at_rule )->add_declarations( $css_declarations ); + static::get_store( $store_name )->add_rule( $css_selector )->add_declarations( $css_declarations ); } /** diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php index 034236ffef1979..9e6798c36a5034 100644 --- a/packages/style-engine/style-engine.php +++ b/packages/style-engine/style-engine.php @@ -30,6 +30,8 @@ * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset||`, to var( --wp--preset--* ) values. Default `false`. * @type string $selector Optional. When a selector is passed, the value of `$css` in the return value will comprise a full CSS rule `$selector { ...$css_declarations }`, * otherwise, the value will be a concatenated string of CSS declarations. + * @type string $container Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module * } * * @return array { @@ -43,6 +45,7 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { $options, array( 'selector' => null, + 'container' => null, 'context' => null, 'convert_vars_to_classnames' => false, ) @@ -54,13 +57,36 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { $styles_output = array(); if ( ! empty( $parsed_styles['declarations'] ) ) { - $styles_output['css'] = WP_Style_Engine::compile_css( $parsed_styles['declarations'], $options['selector'] ); - $styles_output['declarations'] = $parsed_styles['declarations']; - if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $options['selector'], $parsed_styles['declarations'] ); + $container = $options['container'] ?? null; + $selector = $options['selector'] ?? null; + $css_declarations = new WP_Style_Engine_CSS_Declarations( $parsed_styles['declarations'] ); + $new_rule = null; + + // TODO: Extend WP_Style_Engine::compile_css to do this if block. + if ( $selector || $container ) { + if ( $selector ) { + $new_rule = new WP_Style_Engine_CSS_Rule( $options['selector'], $css_declarations ); + } + + if ( $container ) { + $new_rule = new WP_Style_Engine_CSS_Rules_Container( $container, $new_rule ); + if ( ! $selector ) { + $new_rule->add_declarations( $css_declarations ); + } + } + + $styles_output['css'] = $new_rule->get_css(); + } else { + $styles_output['css'] = $css_declarations->get_declarations_string(); + } + + if ( ! empty( $options['context'] ) && $new_rule ) { + WP_Style_Engine::get_store( $options['context'] )->add_rule( $new_rule ); } } + $styles_output['declarations'] = $parsed_styles['declarations']; + if ( ! empty( $parsed_styles['classnames'] ) ) { $styles_output['classnames'] = implode( ' ', array_unique( $parsed_styles['classnames'] ) ); } @@ -83,7 +109,7 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { * Required. A collection of CSS rules. * * @type array ...$0 { - * @type string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @type string $container A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * @type string $selector A CSS selector. * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). * } @@ -113,17 +139,30 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a $css_rule_objects = array(); foreach ( $css_rules as $css_rule ) { - if ( empty( $css_rule['selector'] ) || empty( $css_rule['declarations'] ) || ! is_array( $css_rule['declarations'] ) ) { + if ( empty( $css_rule['declarations'] ) || ! is_array( $css_rule['declarations'] ) ) { continue; } - $at_rule = ! empty( $css_rule['at_rule'] ) ? $css_rule['at_rule'] : ''; + $container = $css_rule['container'] ?? null; + $selector = $css_rule['selector'] ?? null; + $new_rule = null; + + if ( $selector ) { + $new_rule = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'] ); + } + + if ( $container ) { + $new_rule = new WP_Style_Engine_CSS_Rules_Container( $container, $new_rule ); + if ( ! $selector ) { + $new_rule->add_declarations( $css_rule['declarations'] ); + } + } if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $at_rule ); + WP_Style_Engine::get_store( $options['context'] )->add_rule( $new_rule ); } - $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $at_rule ); + $css_rule_objects[] = $new_rule; } if ( empty( $css_rule_objects ) ) { diff --git a/phpunit/style-engine/class-wp-style-engine-css-rules-container-test.php b/phpunit/style-engine/class-wp-style-engine-css-rules-container-test.php new file mode 100644 index 00000000000000..b08d2b33f36f3b --- /dev/null +++ b/phpunit/style-engine/class-wp-style-engine-css-rules-container-test.php @@ -0,0 +1,190 @@ + '50% 50%', + 'color' => 'green', + ) + ); + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( $container_selector, $css_rule ); + + $this->assertSame( $container_selector, $css_container->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); + + $expected = "$container_selector{{$css_rule->get_css()}}"; + + $this->assertSame( $expected, $css_container->get_css(), 'Value returned by get_css() does not match expected CSS string.' ); + } + + /** + * Tests that empty values cannot be added. + * + * @covers ::add_rules + * @covers ::get_rules + */ + public function test_cannot_add_empty_values() { + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@media not all and (hover: hover)' ); + + $css_container->add_rules( '' ); + $this->assertEmpty( $css_container->get_rules(), 'Return value of get_rules() does not match expected rules when empty string added.' ); + + $css_container->add_rules( array() ); + $this->assertEmpty( $css_container->get_rules(), 'Return value of get_rules() does not match expected rules when array() added.' ); + } + + /** + * Tests that nested rule declaration properties are deduplicated. + * + * @covers ::add_rules + * @covers ::get_css + */ + public function test_should_dedupe_properties_in_rules() { + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@media not all and (hover: hover)' ); + $selector = '.goanna'; + $css_rule_1 = new WP_Style_Engine_CSS_Rule_Gutenberg( + $selector, + array( + 'font-size' => '2rem', + ) + ); + $css_container->add_rules( $css_rule_1 ); + + $this->assertSame( '@media not all and (hover: hover){.goanna{font-size:2rem;}}', $css_container->get_css(), 'Return value of get_css() does not match expected CSS container CSS.' ); + + $css_rule_2 = new WP_Style_Engine_CSS_Rule_Gutenberg( + $selector, + array( + 'font-size' => '4px', + ) + ); + $css_container->add_rules( $css_rule_2 ); + + $this->assertSame( '@media not all and (hover: hover){.goanna{font-size:4px;}}', $css_container->get_css(), 'Return value of get_css() does not match expected value with overwritten rule declaration.' ); + } + + /** + * Tests that rules can be added to existing containers. + * + * @covers ::add_rules + * @covers ::get_rule + * @covers ::get_css + */ + public function test_should_add_rules_to_existing_containers() { + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@media screen, print' ); + + $css_rule_1 = new WP_Style_Engine_CSS_Rule_Gutenberg( + 'body', + array( + 'line-height' => '0.1', + ) + ); + $css_container->add_rules( $css_rule_1 ); + + $css_rule_2 = new WP_Style_Engine_CSS_Rule_Gutenberg( + 'p', + array( + 'line-height' => '0.9', + ) + ); + $css_container->add_rules( $css_rule_2 ); + + $this->assertEquals( $css_rule_2, $css_container->get_rule( 'p' ), 'Return value of get_rule() does not match expected value.' ); + + $expected = '@media screen, print{body{line-height:0.1;}p{line-height:0.9;}}'; + + $this->assertSame( $expected, $css_container->get_css(), 'Return value of get_css() does not match expected value.' ); + } + + /** + * Tests setting a selector to a container. + * + * @covers ::set_selector + */ + public function test_should_set_selector() { + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@layer state' ); + + $this->assertSame( '@layer state', $css_container->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); + + $css_container->set_selector( '@layer pony' ); + + $this->assertSame( '@layer pony', $css_container->get_selector(), 'Return value of get_selector() does not match value passed to set_selector().' ); + } + + /** + * Tests generating a CSS rule string. + * + * @covers ::get_css + */ + public function test_should_generate_css_rule_string() { + $selector = '.chips'; + $input_declarations = array( + 'margin-top' => '10px', + 'font-size' => '2rem', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations_Gutenberg( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( $selector, $css_declarations ); + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@layer sauce', $css_rule ); + $expected = "@layer sauce{{$selector}{{$css_declarations->get_declarations_string()}}}"; + + $this->assertSame( $expected, $css_container->get_css() ); + } + + /** + * Tests that an empty string will be returned where there are no rules in a CSS container. + * + * @covers ::get_css + */ + public function test_should_return_empty_string_with_no_rules() { + $selector = '.holmes'; + $input_declarations = array(); + $css_declarations = new WP_Style_Engine_CSS_Declarations_Gutenberg( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( $selector, $css_declarations ); + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@layer sauce', $css_rule ); + + $this->assertSame( '', $css_container->get_css() ); + } + + /** + * Tests that CSS containers are prettified. + * + * @covers ::get_css + */ + public function test_should_prettify_css_rule_output() { + $selector = '.baptiste'; + $input_declarations = array( + 'margin-left' => '0', + 'font-family' => 'Detective Sans', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations_Gutenberg( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( $selector, $css_declarations ); + $expected = '@container (width < 650px) { + .baptiste { + margin-left: 0; + font-family: Detective Sans; + } +}'; + $css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@container (width < 650px)', $css_rule ); + + $this->assertSame( $expected, $css_container->get_css( true ) ); + } +} diff --git a/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php b/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php index 8529bff78e22c8..a1c2c2d79b8a0f 100644 --- a/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php +++ b/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php @@ -140,6 +140,138 @@ public function test_should_add_rule_to_existing_store() { $this->assertSame( $expected, $store_rule->get_css(), 'Return value of get_css() does not match expected CSS from existing store rules.' ); } + /** + * Tests separation of stores. + * + * @covers ::add_rule + * @covers ::get_store + */ + public function test_should_add_rules_to_separate_stores() { + $store_one = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'one' ); + $store_one_rule = $store_one->add_rule( '.one' ); + $store_one_container = $store_one->add_rule( new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '.one_container' ) ); + + $store_two = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'two' ); + $store_rule_two = $store_two->add_rule( '.two' ); + + $this->assertSame( + array( + '.one' => $store_one_rule, + '.one_container' => $store_one_container, + ), + WP_Style_Engine_Gutenberg::get_store( 'one' )->get_all_rules(), + 'get_all_rules() does not return expected array of rules for store one.' + ); + + $this->assertSame( + array( + '.two' => $store_rule_two, + ), + WP_Style_Engine_Gutenberg::get_store( 'two' )->get_all_rules(), + 'get_all_rules() does not return expected array of rules for store two.' + ); + } + + /** + * Tests adding identical selectors. + * + * @covers ::add_rule + */ + public function test_should_not_overwrite_existing_rules() { + $store_one = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'one' ); + $store_one_container = $store_one->add_rule( new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '.one' ) ); + $store_one_rule = $store_one->add_rule( '.one' ); + + $store_two = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'two' ); + $store_rule_two = $store_two->add_rule( '.two' ); + $store_two_container = $store_two->add_rule( new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '.two' ) ); + + $this->assertSame( + $store_one_rule, + $store_one_container, + 'add_rule() does not return already existing return .one rule.' + ); + + $this->assertSame( + $store_two_container, + $store_rule_two, + 'add_rule() does not return already existing return .two rule.' + ); + } + + /** + * Tests adding rules to an existing store. + * + * @covers ::add_rule + */ + public function test_should_combine_existing_rule_objects_to_store() { + $new_hotdog_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'hotdog' ); + + $selector = '.pickle'; + $pickle_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( + $selector, + array( + 'color' => 'brown', + 'border-color' => 'yellow', + 'border-radius' => '10rem', + ) + ); + $pickle_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '.hotdog', $pickle_rule ); + $added_rule = $new_hotdog_store->add_rule( $pickle_container ); + $expected = '.hotdog{.pickle{color:brown;border-color:yellow;border-radius:10rem;}}'; + + $this->assertSame( $expected, $added_rule->get_css(), 'Return value of store rule get_css() matches passed rule object get_css().' ); + + $pickle_container_2 = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( + '.hotdog', + array( + new WP_Style_Engine_CSS_Rule_Gutenberg( + '.pickle-2', + array( + 'color' => 'pink', + 'border-color' => 'blue', + 'border-radius' => '11rem', + ) + ), + new WP_Style_Engine_CSS_Rule_Gutenberg( + $selector, + array( + 'border-radius' => '1px', + ) + ), + ) + ); + $pickle_container->add_declarations( array( 'padding' => '100px' ) ); + $added_rule = $new_hotdog_store->add_rule( $pickle_container_2 ); + $expected = '.hotdog{padding:100px;.pickle{color:brown;border-color:yellow;border-radius:1px;}.pickle-2{color:pink;border-color:blue;border-radius:11rem;}}'; + $this->assertSame( $expected, $added_rule->get_css(), 'Return value of store rule get_css() matches passed rule object get_css().' ); + } + + + /** + * Tests adding rules to an existing store. + * + * @covers ::add_rule + */ + public function test_should_add_rule_object_to_existing_store() { + $new_hotdog_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'hotdog' ); + + $selector = '.pickle'; + $pickle_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( + $selector, + array( + 'color' => 'brown', + 'border-color' => 'yellow', + 'border-radius' => '10rem', + ) + ); + $pickle_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '.hotdog', $pickle_rule ); + $added_rule = $new_hotdog_store->add_rule( $pickle_container ); + $expected = '.hotdog{.pickle{color:brown;border-color:yellow;border-radius:10rem;}}'; + + $this->assertSame( $expected, $added_rule->get_css(), 'Return value of store rule get_css() matches passed rule object get_css().' ); + } + /** * Tests that all stored rule objects are returned. * diff --git a/phpunit/style-engine/class-wp-style-engine-processor-test.php b/phpunit/style-engine/class-wp-style-engine-processor-test.php index a1ec32a20977c8..ba74713ec44f08 100644 --- a/phpunit/style-engine/class-wp-style-engine-processor-test.php +++ b/phpunit/style-engine/class-wp-style-engine-processor-test.php @@ -44,41 +44,6 @@ public function test_should_return_rules_as_compiled_css() { ); } - /** - * Tests adding nested rules with at-rules and returning compiled CSS rules. - * - * @covers ::add_rules - * @covers ::get_css - */ - public function test_should_return_nested_rules_as_compiled_css() { - $a_nice_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nice-rule' ); - $a_nice_css_rule->add_declarations( - array( - 'color' => 'var(--nice-color)', - 'background-color' => 'purple', - ) - ); - $a_nice_css_rule->set_at_rule( '@media (min-width: 80rem)' ); - - $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nicer-rule' ); - $a_nicer_css_rule->add_declarations( - array( - 'font-family' => 'Nice sans', - 'font-size' => '1em', - 'background-color' => 'purple', - ) - ); - $a_nicer_css_rule->set_at_rule( '@layer nicety' ); - - $a_nice_processor = new WP_Style_Engine_Processor_Gutenberg(); - $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); - - $this->assertSame( - '@media (min-width: 80rem){.a-nice-rule{color:var(--nice-color);background-color:purple;}}@layer nicety{.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}}', - $a_nice_processor->get_css( array( 'prettify' => false ) ) - ); - } - /** * Tests compiling CSS rules and formatting them with new lines and indents. * @@ -130,52 +95,6 @@ public function test_should_return_prettified_css_rules() { ); } - /** - * Tests compiling nested CSS rules and formatting them with new lines and indents. - * - * @covers ::get_css - */ - public function test_should_return_prettified_nested_css_rules() { - $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-wonderful-rule' ); - $a_wonderful_css_rule->add_declarations( - array( - 'color' => 'var(--wonderful-color)', - 'background-color' => 'orange', - ) - ); - $a_wonderful_css_rule->set_at_rule( '@media (min-width: 80rem)' ); - - $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-very_wonderful-rule' ); - $a_very_wonderful_css_rule->add_declarations( - array( - 'color' => 'var(--wonderful-color)', - 'background-color' => 'orange', - ) - ); - $a_very_wonderful_css_rule->set_at_rule( '@layer wonderfulness' ); - - $a_wonderful_processor = new WP_Style_Engine_Processor_Gutenberg(); - $a_wonderful_processor->add_rules( array( $a_wonderful_css_rule, $a_very_wonderful_css_rule ) ); - - $expected = '@media (min-width: 80rem) { - .a-wonderful-rule { - color: var(--wonderful-color); - background-color: orange; - } -} -@layer wonderfulness { - .a-very_wonderful-rule { - color: var(--wonderful-color); - background-color: orange; - } -} -'; - $this->assertSame( - $expected, - $a_wonderful_processor->get_css( array( 'prettify' => true ) ) - ); - } - /** * Tests adding a store and compiling CSS rules from that store. * @@ -393,4 +312,162 @@ public function test_should_combine_previously_added_css_rules() { 'Return value of get_css() does not match expectations when combining 4 CSS rules' ); } + + /** + * Tests adding nested rules with at-rules and returning compiled CSS rules. + * + * @covers ::add_rules + * @covers ::get_css + */ + public function test_should_return_nested_rules_as_compiled_css() { + $a_nice_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nice-rule' ); + $a_nice_css_rule->add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nice_css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@media (min-width: 80rem)', $a_nice_css_rule ); + + $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nicer-rule' ); + $a_nicer_css_rule->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nicer_css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@layer nicety', $a_nicer_css_rule ); + + $a_nice_processor = new WP_Style_Engine_Processor_Gutenberg(); + $a_nice_processor->add_rules( array( $a_nice_css_container, $a_nicer_css_container ) ); + + $this->assertSame( + '@media (min-width: 80rem){.a-nice-rule{color:var(--nice-color);background-color:purple;}}@layer nicety{.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}}', + $a_nice_processor->get_css( array( 'prettify' => false ) ) + ); + } + + /** + * Tests compiling nested CSS rules and formatting them with new lines and indents. + * + * @covers ::get_css + */ + public function test_should_return_prettified_nested_css_rules() { + // Create nested CSS rule 1. + $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-wonderful-rule' ); + $a_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_wonderful_css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@media (min-width: 80rem)', $a_wonderful_css_rule ); + + // Create nested CSS rule 2. + $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-very_wonderful-rule' ); + $a_very_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_very_wonderful_css_container = new WP_Style_Engine_CSS_Rules_Container_Gutenberg( '@layer wonderfulness', $a_very_wonderful_css_rule ); + + $a_wonderful_processor = new WP_Style_Engine_Processor_Gutenberg(); + $a_wonderful_processor->add_rules( array( $a_wonderful_css_container, $a_very_wonderful_css_container ) ); + + $expected = '@media (min-width: 80rem) { + .a-wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +@layer wonderfulness { + .a-very_wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +'; + $this->assertSame( + $expected, + $a_wonderful_processor->get_css( array( 'prettify' => true ) ) + ); + } + + /** + * Tests that incoming CSS rules are merged with existing CSS rules and that containers appear at the end of the CSS output. + * + * @covers ::add_rules + * @covers ::get_rules + */ + public function test_should_return_compiled_css_with_nested_rules_last() { + $panda_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'panda' ); + // Add a nested rule. + $panda_store->add_rule( + new WP_Style_Engine_CSS_Rules_Container_Gutenberg( + '@container (width > 400px)', + new WP_Style_Engine_CSS_Rule_Gutenberg( + '.blueberry', + array( + 'background-color' => 'blue', + ) + ) + ) + ); + + // Add a regular rule. + $panda_store_rule = $panda_store->add_rule( '.wp-block-mushroom a:hover' ); + $panda_store_rule->add_declarations( + array( + 'padding' => '100px', + ) + ); + + $a_panda_renderer = new WP_Style_Engine_Processor_Gutenberg(); + $a_panda_renderer->add_store( $panda_store ); + $this->assertSame( + '.wp-block-mushroom a:hover{padding:100px;}@container (width > 400px){.blueberry{background-color:blue;}}', + $a_panda_renderer->get_css( array( 'prettify' => false ) ), + 'Returns processed CSS rules with containers and containers at the end of the CSS output.' + ); + + // Combine existing nested rule with one added directly to the processor. + $a_panda_renderer->add_rules( + new WP_Style_Engine_CSS_Rules_Container_Gutenberg( + '@container (width > 400px)', + new WP_Style_Engine_CSS_Rule_Gutenberg( + '.raspberry', + array( + 'background-color' => 'red', + ) + ) + ) + ); + + $this->assertSame( + '.wp-block-mushroom a:hover{padding:100px;}@container (width > 400px){.blueberry{background-color:blue;}.raspberry{background-color:red;}}', + $a_panda_renderer->get_css( array( 'prettify' => false ) ), + 'Returns processed CSS rules with rules added to containers.' + ); + + $expected_prettified = '.wp-block-mushroom a:hover { + padding: 100px; +} +@container (width > 400px) { + .blueberry { + background-color: blue; + } + .raspberry { + background-color: red; + } +} +'; + $this->assertSame( + $expected_prettified, + $a_panda_renderer->get_css( array( 'prettify' => true ) ), + 'Returns prettified processed CSS rules and nested rules' + ); + } } diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index b66ccf5694bd66..4cb463d3a5a278 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -23,7 +23,7 @@ public function tear_down() { /** * Tests generating block styles and classnames based on various manifestations of the $block_styles argument. * - * @covers ::gutenberg_style_engine_get_styles + * @covers ::wp_style_engine_get_styles * @covers WP_Style_Engine_Gutenberg::parse_block_styles * @covers WP_Style_Engine_Gutenberg::compile_css * @@ -236,6 +236,32 @@ public function data_wp_style_engine_get_styles() { ), ), + 'style_block_with_nested_selector' => array( + 'block_styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ), + 'options' => array( + 'selector' => '.wp-selector > p', + 'container' => '@layer sandwich', + ), + 'expected_output' => array( + 'css' => '@layer sandwich{.wp-selector > p{padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;}}', + 'declarations' => array( + 'padding-top' => '42px', + 'padding-left' => '2%', + 'padding-bottom' => '44px', + 'padding-right' => '5rem', + ), + ), + ), + 'elements_with_css_var_value' => array( 'block_styles' => array( 'color' => array( @@ -531,7 +557,7 @@ public function data_wp_style_engine_get_styles() { /** * Tests adding rules to a store and retrieving a generated stylesheet. * - * @covers ::gutenberg_style_engine_get_styles + * @covers ::wp_style_engine_get_styles * @covers WP_Style_Engine_Gutenberg::store_css_rule */ public function test_should_store_block_styles_using_context() { @@ -559,6 +585,49 @@ public function test_should_store_block_styles_using_context() { $this->assertSame( $generated_styles['css'], $rule->get_css() ); } + /** + * Tests adding nested rules/containers to a store and retrieving a generated stylesheet. + * + * @covers ::wp_style_engine_get_styles + * @covers ::wp_style_engine_get_stylesheet_from_context + * @covers WP_Style_Engine_Gutenberg::store_css_rule + */ + public function test_should_store_nested_block_styles_using_context() { + gutenberg_style_engine_get_styles( + array( + 'color' => array( + 'text' => 'var:preset|color|texas-flood', + ), + ), + array( + 'context' => 'block-supports', + 'container' => 'main', + ) + ); + + gutenberg_style_engine_get_styles( + array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ), + array( + 'context' => 'block-supports', + 'selector' => '& .container', + 'container' => 'main', + ) + ); + + $store_css = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertSame( 'main{color:var(--wp--preset--color--texas-flood);& .container{padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;}}', $store_css ); + } + /** * Tests that passing no context does not store styles. * @@ -589,7 +658,40 @@ public function test_should_not_store_block_styles_without_context() { * @covers ::wp_style_engine_get_stylesheet_from_context */ public function test_should_get_stored_stylesheet_from_context() { - $css_rules = array( + $css_rules = array( + array( + 'container' => '.gollem', + 'declarations' => array( + 'background-color' => 'green', + ), + ), + array( + 'container' => '.gollem', + 'selector' => '&.precious:hover', + 'declarations' => array( + 'background-color' => 'gold', + ), + ), + array( + 'selector' => '.pippin', + 'container' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'brown', + 'height' => '12px', + 'width' => '15px', + 'border-style' => 'dashed', + ), + ), + array( + 'selector' => '.merry', + 'container' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'khaki', + 'height' => '23px', + 'width' => '23px', + 'border-style' => 'solid', + ), + ), array( 'selector' => '.frodo', 'declarations' => array( @@ -608,7 +710,16 @@ public function test_should_get_stored_stylesheet_from_context() { 'border-style' => 'solid', ), ), + array( + 'selector' => '.pippin', + 'container' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'tan', + ), + ), ); + + // Unit tests for the `wp_style_engine_get_stylesheet_from_css_rules` function below. $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( @@ -622,7 +733,7 @@ public function test_should_get_stored_stylesheet_from_context() { /** * Tests returning a generated stylesheet from a set of rules. * - * @covers ::gutenberg_style_engine_get_stylesheet_from_css_rules + * @covers ::wp_style_engine_get_stylesheet_from_css_rules * @covers WP_Style_Engine_Gutenberg::compile_stylesheet_from_css_rules */ public function test_should_return_stylesheet_from_css_rules() { @@ -661,12 +772,127 @@ public function test_should_return_stylesheet_from_css_rules() { $this->assertSame( '.saruman{color:white;height:100px;border-style:solid;align-self:unset;}.gandalf{color:grey;height:90px;border-style:dotted;align-self:safe center;}.radagast{color:brown;height:60px;border-style:dashed;align-self:stretch;}', $compiled_stylesheet ); } + /** + * Tests returning a generated stylesheet from a set of nested rules. + */ + public function test_should_return_stylesheet_with_combined_nested_css_rules_printed_after_non_nested() { + $css_rules = array( + array( + 'container' => '.sauron', + 'declarations' => array( + 'text-transform' => 'uppercase', + ), + ), + array( + 'container' => '.sauron', + 'selector' => '.witch-king', + 'declarations' => array( + 'text-transform' => 'lowercase', + ), + ), + array( + 'selector' => '.saruman', + 'declarations' => array( + 'letter-spacing' => '1px', + ), + ), + array( + 'selector' => '.saruman', + 'container' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.saruman', + 'container' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'black', + 'font-family' => 'The-Great-Eye', + ), + ), + array( + 'selector' => '.voldemort', + 'container' => '@supports (align-self: stretch)', + 'declarations' => array( + 'height' => '100px', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'letter-spacing' => '2px', + ), + ), + array( + 'selector' => '.gandalf', + 'container' => '@supports (border-style: dotted)', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + 'align-self' => 'safe center', + ), + ), + array( + 'selector' => '.radagast', + 'container' => '@supports (align-self: stretch)', + 'declarations' => array( + 'color' => 'brown', + 'height' => '60px', + 'border-style' => 'dashed', + 'align-self' => 'stretch', + ), + ), + ); + + $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.saruman{letter-spacing:1px;}.gandalf{letter-spacing:2px;}.sauron{text-transform:uppercase;.witch-king{text-transform:lowercase;}}@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}@supports (align-self: stretch){.voldemort{height:100px;align-self:stretch;}.radagast{color:brown;height:60px;border-style:dashed;align-self:stretch;}}@supports (border-style: dotted){.gandalf{color:grey;height:90px;border-style:dotted;align-self:safe center;}}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules. + */ + public function test_should_return_stylesheet_with_nested_at_rules() { + $css_rules = array( + array( + 'container' => '.foo', + 'declarations' => array( + 'background-color' => 'red', + ), + ), + array( + 'container' => '.foo', + 'selector' => '@media (orientation: landscape)', + 'declarations' => array( + 'background-color' => 'blue', + ), + ), + array( + 'container' => '.foo', + 'selector' => '@media (min-width > 1024px)', + 'declarations' => array( + 'background-color' => 'cotton-blue', + ), + ), + ); + + $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.foo{background-color:red;@media (orientation: landscape){background-color:blue;}@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); + } + /** * Tests that incoming styles are deduped and merged. * * @ticket 58811 * - * @covers ::gutenberg_style_engine_get_stylesheet_from_css_rules + * @covers ::wp_style_engine_get_stylesheet_from_css_rules * @covers WP_Style_Engine_Gutenberg::compile_stylesheet_from_css_rules */ public function test_should_dedupe_and_merge_css_rules() { @@ -716,7 +942,7 @@ public function test_should_dedupe_and_merge_css_rules() { * * This is testing this fix: https://github.com/WordPress/gutenberg/pull/49004 * - * @covers ::gutenberg_style_engine_get_stylesheet_from_css_rules + * @covers ::wp_style_engine_get_stylesheet_from_css_rules * @covers WP_Style_Engine_Gutenberg::compile_stylesheet_from_css_rules */ public function test_should_return_stylesheet_from_duotone_css_rules() { diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 0a4b8cef574464..37cbe4955034df 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -44,6 +44,7 @@ const bundledPackagesPhpConfig = [ replaceClasses: [ 'WP_Style_Engine_CSS_Declarations', 'WP_Style_Engine_CSS_Rules_Store', + 'WP_Style_Engine_CSS_Rules_Container', 'WP_Style_Engine_CSS_Rule', 'WP_Style_Engine_Processor', 'WP_Style_Engine',