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 4a7a57f107cd2f..18d8c2f7ef7f0f 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -32,16 +32,27 @@ 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() ) { + public function __construct( $selector = '', $declarations = array(), $at_rule = '' ) { $this->set_selector( $selector ); $this->add_declarations( $declarations ); + $this->set_at_rule( $at_rule ); } /** @@ -80,6 +91,18 @@ 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. * @@ -98,6 +121,15 @@ 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. * @@ -107,19 +139,28 @@ public function get_selector() { * @return string */ public function get_css( $should_prettify = false, $indent_count = 0 ) { - $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; - $declarations_indent = $should_prettify ? $indent_count + 1 : 0; - $suffix = $should_prettify ? "\n" : ''; - $spacer = $should_prettify ? ' ' : ''; + $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 ? ' ' : ''; // 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; - $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $declarations_indent ); + $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 ); 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-store.php b/packages/style-engine/class-wp-style-engine-css-rules-store.php index 5a5c2f0dac2fa9..e199cda5da0fcb 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 @@ -110,17 +110,26 @@ public function get_all_rules() { * 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`. * * @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 ) { + 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 ) ) { 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 ); + } + 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 ); diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index a57887a75f379b..3f8cef0a2cf315 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -66,13 +66,29 @@ public function add_rules( $css_rules ) { foreach ( $css_rules as $rule ) { $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( $at_rule ) ) { + if ( isset( $this->css_rules[ "$at_rule $selector" ] ) ) { + $this->css_rules[ "$at_rule $selector" ]->add_declarations( $rule->get_declarations() ); + continue; + } + $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; } $this->css_rules[ $rule->get_selector() ] = $rule; } - return $this; } @@ -110,6 +126,7 @@ 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. $css .= $rule->get_css( $options['prettify'] ); $css .= $options['prettify'] ? "\n" : ''; } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index b0c4b7f0002424..a97e7cb8a0b213 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 ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations, $css_at_rule = '' ) { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector )->add_declarations( $css_declarations ); + static::get_store( $store_name )->add_rule( $css_selector, $css_at_rule )->add_declarations( $css_declarations ); } /** diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php index 4571a2fcce4ffe..034236ffef1979 100644 --- a/packages/style-engine/style-engine.php +++ b/packages/style-engine/style-engine.php @@ -83,6 +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 $selector A CSS selector. * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). * } @@ -116,11 +117,13 @@ 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'] : ''; + if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'] ); + WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $at_rule ); } - $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'] ); + $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $at_rule ); } if ( empty( $css_rule_objects ) ) { 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 4177ab276f1c8c..a1ec32a20977c8 100644 --- a/phpunit/style-engine/class-wp-style-engine-processor-test.php +++ b/phpunit/style-engine/class-wp-style-engine-processor-test.php @@ -44,6 +44,41 @@ 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. * @@ -95,6 +130,52 @@ 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. *