diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md index 290e04ee60ae83..f26f8ff7e779b0 100644 --- a/packages/style-engine/README.md +++ b/packages/style-engine/README.md @@ -126,10 +126,31 @@ $styles = array( 'selector' => '.wp-tomato', 'declarations' => array( 'padding' => '100px' ) ), +); + +$stylesheet = wp_style_engine_get_stylesheet_from_css_rules( + $styles, array( - 'selector' => '.wp-kumquat', + 'context' => 'block-supports', // Indicates that these styles should be stored with block supports CSS. + ) +); +print_r( $stylesheet ); // .wp-pumpkin{color:orange}.wp-tomato{color:red;padding:100px} +``` + +It's also possible to build simple, nested CSS rules using the `rules_group` key. + +```php +$styles = array( + array( + 'rules_group' => '@media (min-width: 80rem)', + 'selector' => '.wp-carrot', 'declarations' => array( 'color' => 'orange' ) ), + array( + 'rules_group' => '@media (min-width: 80rem)', + 'selector' => '.wp-tomato', + 'declarations' => array( 'color' => 'red' ) + ), ); $stylesheet = wp_style_engine_get_stylesheet_from_css_rules( @@ -138,7 +159,7 @@ $stylesheet = wp_style_engine_get_stylesheet_from_css_rules( 'context' => 'block-supports', // Indicates that these styles should be stored with block supports CSS. ) ); -print_r( $stylesheet ); // .wp-pumpkin,.wp-kumquat{color:orange}.wp-tomato{color:red;padding:100px} +print_r( $stylesheet ); // @media (min-width: 80rem){.wp-carrot{color:orange}}@media (min-width: 80rem){.wp-tomato{color:red;}} ``` ### wp_style_engine_get_stylesheet_from_context() 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..12fe5fab23f723 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -33,12 +33,11 @@ class WP_Style_Engine_CSS_Rule { protected $declarations; /** - * The CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`.. * * @var string */ - protected $at_rule; - + protected $rules_group; /** * Constructor @@ -46,13 +45,13 @@ class WP_Style_Engine_CSS_Rule { * @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`. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or 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(), $rules_group = '' ) { $this->set_selector( $selector ); $this->add_declarations( $declarations ); - $this->set_at_rule( $at_rule ); + $this->set_rules_group( $rules_group ); } /** @@ -92,17 +91,26 @@ public function add_declarations( $declarations ) { } /** - * Sets the at_rule. + * Sets the rules group. * - * @param string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @param string $rules_group 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 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; + public function set_rules_group( $rules_group ) { + $this->rules_group = $rules_group; return $this; } + /** + * Gets the rules group. + * + * @return string + */ + public function get_rules_group() { + return $this->rules_group; + } + /** * Gets the declarations object. * @@ -121,15 +129,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. * @@ -148,16 +147,16 @@ public function get_css( $should_prettify = false, $indent_count = 0 ) { // 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 ); + $rules_group = $this->get_rules_group(); + $has_rules_group = ! empty( $rules_group ); + $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_rules_group ? $nested_declarations_indent : $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}}"; + if ( $has_rules_group ) { + $selector = "{$rule_indent}{$rules_group}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}"; return $selector; } 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..50d1cff4142523 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,25 +109,25 @@ 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 $selector The CSS selector. + * @param string $rules_group 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 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 ); + public function add_rule( $selector, $rules_group = '' ) { + $selector = $selector ? trim( $selector ) : ''; + $rules_group = $rules_group ? trim( $rules_group ) : ''; // Bail early if there is no selector. if ( empty( $selector ) ) { 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 ( ! empty( $rules_group ) ) { + if ( empty( $this->rules[ "$rules_group $selector" ] ) ) { + $this->rules[ "$rules_group $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $rules_group ); } - return $this->rules[ "$at_rule $selector" ]; + return $this->rules[ "$rules_group $selector" ]; } // Create the rule if it doesn't exist. diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index 3f8cef0a2cf315..728b8029f26900 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -65,20 +65,20 @@ public function add_rules( $css_rules ) { } foreach ( $css_rules as $rule ) { - $selector = $rule->get_selector(); - $at_rule = $rule->get_at_rule(); + $selector = $rule->get_selector(); + $rules_group = $rule->get_rules_group(); /** * 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( $at_rule ) ) { - if ( isset( $this->css_rules[ "$at_rule $selector" ] ) ) { - $this->css_rules[ "$at_rule $selector" ]->add_declarations( $rule->get_declarations() ); + if ( ! empty( $rules_group ) ) { + if ( isset( $this->css_rules[ "$rules_group $selector" ] ) ) { + $this->css_rules[ "$rules_group $selector" ]->add_declarations( $rule->get_declarations() ); continue; } - $this->css_rules[ "$at_rule $selector" ] = $rule; + $this->css_rules[ "$rules_group $selector" ] = $rule; continue; } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index a97e7cb8a0b213..23818eb889d015 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -355,14 +355,15 @@ protected static function is_valid_style_value( $style_value ) { * @param string $store_name A valid store key. * @param string $css_selector When a selector is passed, the function will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. * @param string[] $css_declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * @param string $rules_group 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 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, $rules_group = '' ) { 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, $rules_group )->add_declarations( $css_declarations ); } /** diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php index 034236ffef1979..74df1b1040f6c7 100644 --- a/packages/style-engine/style-engine.php +++ b/packages/style-engine/style-engine.php @@ -83,7 +83,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 $rules_group 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" ). * } @@ -117,13 +117,12 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a continue; } - $at_rule = ! empty( $css_rule['at_rule'] ) ? $css_rule['at_rule'] : ''; - + $rules_group = $css_rule['rules_group'] ?? null; if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $at_rule ); + WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $rules_group ); } - $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $at_rule ); + $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group ); } if ( empty( $css_rule_objects ) ) { diff --git a/phpunit/style-engine/class-wp-style-engine-css-rule-test.php b/phpunit/style-engine/class-wp-style-engine-css-rule-test.php index 444c83f150f911..3a513ebb8b3bdd 100644 --- a/phpunit/style-engine/class-wp-style-engine-css-rule-test.php +++ b/phpunit/style-engine/class-wp-style-engine-css-rule-test.php @@ -34,6 +34,22 @@ public function test_should_instantiate_with_selector_and_rules() { $this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' ); } + /** + * Tests setting and getting a rules group. + * + * @covers ::set_rules_group + * @covers ::get_rules_group + */ + public function test_should_set_rules_group() { + $rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.heres-johnny', array(), '@layer state' ); + + $this->assertSame( '@layer state', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to constructor.' ); + + $rule->set_rules_group( '@layer pony' ); + + $this->assertSame( '@layer pony', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to set_rules_group().' ); + } + /** * Tests that declaration properties are deduplicated. * 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..297e05b5580d58 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 @@ -170,4 +170,20 @@ public function test_should_get_all_rule_objects_for_a_store() { $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations after adding new rules to store.' ); } + + /** + * Tests adding rules group keys to store. + * + * @covers ::add_rule + */ + public function test_should_store_as_concatenated_rules_groups_and_selector() { + $store_one = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'one' ); + $store_one_rule = $store_one->add_rule( '.tony', '.one' ); + + $this->assertSame( + '.one .tony', + "{$store_one_rule->get_rules_group()} {$store_one_rule->get_selector()}", + 'add_rule() does not concatenate rules group and selector.' + ); + } } 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..6d3a5a812e4fd3 100644 --- a/phpunit/style-engine/class-wp-style-engine-processor-test.php +++ b/phpunit/style-engine/class-wp-style-engine-processor-test.php @@ -58,7 +58,7 @@ public function test_should_return_nested_rules_as_compiled_css() { 'background-color' => 'purple', ) ); - $a_nice_css_rule->set_at_rule( '@media (min-width: 80rem)' ); + $a_nice_css_rule->set_rules_group( '@media (min-width: 80rem)' ); $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nicer-rule' ); $a_nicer_css_rule->add_declarations( @@ -68,7 +68,7 @@ public function test_should_return_nested_rules_as_compiled_css() { 'background-color' => 'purple', ) ); - $a_nicer_css_rule->set_at_rule( '@layer nicety' ); + $a_nicer_css_rule->set_rules_group( '@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 ) ); @@ -143,7 +143,7 @@ public function test_should_return_prettified_nested_css_rules() { 'background-color' => 'orange', ) ); - $a_wonderful_css_rule->set_at_rule( '@media (min-width: 80rem)' ); + $a_wonderful_css_rule->set_rules_group( '@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( @@ -152,7 +152,7 @@ public function test_should_return_prettified_nested_css_rules() { 'background-color' => 'orange', ) ); - $a_very_wonderful_css_rule->set_at_rule( '@layer wonderfulness' ); + $a_very_wonderful_css_rule->set_rules_group( '@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 ) ); diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index b66ccf5694bd66..63e5b3c1c7f7f0 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 * @@ -531,7 +531,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() { @@ -666,7 +666,7 @@ public function test_should_return_stylesheet_from_css_rules() { * * @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 +716,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() { @@ -736,4 +736,60 @@ public function test_should_return_stylesheet_from_duotone_css_rules() { $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); $this->assertSame( ".wp-duotone-ffffff-000000-1{filter:url('#wp-duotone-ffffff-000000-1') !important;}", $compiled_stylesheet ); } + + /** + * Tests returning a generated stylesheet from a set of nested rules and merging their declarations. + */ + public function test_should_merge_declarations_for_rules_groups() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'black', + 'font-family' => 'The-Great-Eye', + ), + ), + ); + + $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules. + */ + public function test_should_return_stylesheet_with_nested_rules() { + $css_rules = array( + array( + 'rules_group' => '.foo', + 'selector' => '@media (orientation: landscape)', + 'declarations' => array( + 'background-color' => 'blue', + ), + ), + array( + 'rules_group' => '.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{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); + } }