Skip to content

Commit 6b328e6

Browse files
Editor: add Style Engine support for nested CSS rules.
Adds support for passing a `$rules_group` string to wp_style_engine_get_stylesheet_from_css_rules(), so rules can be nested under a media query, layer or other rule. Props isabel_brison, ramonopoly. Fixes #61099. git-svn-id: https://develop.svn.wordpress.org/trunk@58089 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 65b4b6a commit 6b328e6

9 files changed

Lines changed: 286 additions & 14 deletions

src/wp-includes/style-engine.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,14 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) {
113113
* .elephant-are-cool{color:gray;width:3em}
114114
*
115115
* @since 6.1.0
116+
* @since 6.6.0 Added support for `$rules_group` in the `$css_rules` array.
116117
*
117118
* @param array $css_rules {
118119
* Required. A collection of CSS rules.
119120
*
120121
* @type array ...$0 {
122+
* @type string $rules_group A parent CSS selector in the case of nested CSS,
123+
* or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`.
121124
* @type string $selector A CSS selector.
122125
* @type string[] $declarations An associative array of CSS definitions,
123126
* e.g. `array( "$property" => "$value", "$property" => "$value" )`.
@@ -154,11 +157,12 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a
154157
continue;
155158
}
156159

160+
$rules_group = $css_rule['rules_group'] ?? null;
157161
if ( ! empty( $options['context'] ) ) {
158-
WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'] );
162+
WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $rules_group );
159163
}
160164

161-
$css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'] );
165+
$css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group );
162166
}
163167

164168
if ( empty( $css_rule_objects ) ) {

src/wp-includes/style-engine/class-wp-style-engine-css-rule.php

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,33 @@ class WP_Style_Engine_CSS_Rule {
3535
*/
3636
protected $declarations;
3737

38+
/**
39+
* A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
40+
* such as `@media (min-width: 80rem)` or `@layer module`.
41+
*
42+
* @since 6.6.0
43+
* @var string
44+
*/
45+
protected $rules_group;
46+
3847
/**
3948
* Constructor.
4049
*
4150
* @since 6.1.0
51+
* @since 6.6.0 Added the `$rules_group` parameter.
4252
*
4353
* @param string $selector Optional. The CSS selector. Default empty string.
4454
* @param string[]|WP_Style_Engine_CSS_Declarations $declarations Optional. An associative array of CSS definitions,
4555
* e.g. `array( "$property" => "$value", "$property" => "$value" )`,
4656
* or a WP_Style_Engine_CSS_Declarations object.
4757
* Default empty array.
58+
* @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
59+
* such as `@media (min-width: 80rem)` or `@layer module`.
4860
*/
49-
public function __construct( $selector = '', $declarations = array() ) {
61+
public function __construct( $selector = '', $declarations = array(), $rules_group = '' ) {
5062
$this->set_selector( $selector );
5163
$this->add_declarations( $declarations );
64+
$this->set_rules_group( $rules_group );
5265
}
5366

5467
/**
@@ -89,6 +102,31 @@ public function add_declarations( $declarations ) {
89102
return $this;
90103
}
91104

105+
/**
106+
* Sets the rules group.
107+
*
108+
* @since 6.6.0
109+
*
110+
* @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
111+
* such as `@media (min-width: 80rem)` or `@layer module`.
112+
* @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods.
113+
*/
114+
public function set_rules_group( $rules_group ) {
115+
$this->rules_group = $rules_group;
116+
return $this;
117+
}
118+
119+
/**
120+
* Gets the rules group.
121+
*
122+
* @since 6.6.0
123+
*
124+
* @return string
125+
*/
126+
public function get_rules_group() {
127+
return $this->rules_group;
128+
}
129+
92130
/**
93131
* Gets the declarations object.
94132
*
@@ -115,6 +153,7 @@ public function get_selector() {
115153
* Gets the CSS.
116154
*
117155
* @since 6.1.0
156+
* @since 6.6.0 Added support for nested CSS with rules groups.
118157
*
119158
* @param bool $should_prettify Optional. Whether to add spacing, new lines and indents.
120159
* Default false.
@@ -123,17 +162,28 @@ public function get_selector() {
123162
* @return string
124163
*/
125164
public function get_css( $should_prettify = false, $indent_count = 0 ) {
126-
$rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : '';
127-
$declarations_indent = $should_prettify ? $indent_count + 1 : 0;
128-
$suffix = $should_prettify ? "\n" : '';
129-
$spacer = $should_prettify ? ' ' : '';
130-
$selector = $should_prettify ? str_replace( ',', ",\n", $this->get_selector() ) : $this->get_selector();
131-
$css_declarations = $this->declarations->get_declarations_string( $should_prettify, $declarations_indent );
165+
$rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : '';
166+
$nested_rule_indent = $should_prettify ? str_repeat( "\t", $indent_count + 1 ) : '';
167+
$declarations_indent = $should_prettify ? $indent_count + 1 : 0;
168+
$nested_declarations_indent = $should_prettify ? $indent_count + 2 : 0;
169+
$suffix = $should_prettify ? "\n" : '';
170+
$spacer = $should_prettify ? ' ' : '';
171+
// Trims any multiple selectors strings.
172+
$selector = $should_prettify ? implode( ',', array_map( 'trim', explode( ',', $this->get_selector() ) ) ) : $this->get_selector();
173+
$selector = $should_prettify ? str_replace( array( ',' ), ",\n", $selector ) : $selector;
174+
$rules_group = $this->get_rules_group();
175+
$has_rules_group = ! empty( $rules_group );
176+
$css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_rules_group ? $nested_declarations_indent : $declarations_indent );
132177

133178
if ( empty( $css_declarations ) ) {
134179
return '';
135180
}
136181

182+
if ( $has_rules_group ) {
183+
$selector = "{$rule_indent}{$rules_group}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}";
184+
return $selector;
185+
}
186+
137187
return "{$rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$rule_indent}}";
138188
}
139189
}

src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,30 @@ public function get_all_rules() {
121121
* If the rule does not exist, it will be created.
122122
*
123123
* @since 6.1.0
124+
* @since 6.6.0 Added the $rules_group parameter.
124125
*
125126
* @param string $selector The CSS selector.
127+
* @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
128+
* such as `@media (min-width: 80rem)` or `@layer module`.
126129
* @return WP_Style_Engine_CSS_Rule|void Returns a WP_Style_Engine_CSS_Rule object,
127130
* or void if the selector is empty.
128131
*/
129-
public function add_rule( $selector ) {
130-
$selector = trim( $selector );
132+
public function add_rule( $selector, $rules_group = '' ) {
133+
$selector = $selector ? trim( $selector ) : '';
134+
$rules_group = $rules_group ? trim( $rules_group ) : '';
131135

132136
// Bail early if there is no selector.
133137
if ( empty( $selector ) ) {
134138
return;
135139
}
136140

141+
if ( ! empty( $rules_group ) ) {
142+
if ( empty( $this->rules[ "$rules_group $selector" ] ) ) {
143+
$this->rules[ "$rules_group $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $rules_group );
144+
}
145+
return $this->rules[ "$rules_group $selector" ];
146+
}
147+
137148
// Create the rule if it doesn't exist.
138149
if ( empty( $this->rules[ $selector ] ) ) {
139150
$this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector );

src/wp-includes/style-engine/class-wp-style-engine-processor.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function add_store( $store ) {
5858
* Adds rules to be processed.
5959
*
6060
* @since 6.1.0
61+
* @since 6.6.0 Added support for rules_group.
6162
*
6263
* @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[] $css_rules A single, or an array of,
6364
* WP_Style_Engine_CSS_Rule objects
@@ -70,7 +71,24 @@ public function add_rules( $css_rules ) {
7071
}
7172

7273
foreach ( $css_rules as $rule ) {
73-
$selector = $rule->get_selector();
74+
$selector = $rule->get_selector();
75+
$rules_group = $rule->get_rules_group();
76+
77+
/**
78+
* If there is a rules_group and it already exists in the css_rules array,
79+
* add the rule to it.
80+
* Otherwise, create a new entry for the rules_group.
81+
*/
82+
if ( ! empty( $rules_group ) ) {
83+
if ( isset( $this->css_rules[ "$rules_group $selector" ] ) ) {
84+
$this->css_rules[ "$rules_group $selector" ]->add_declarations( $rule->get_declarations() );
85+
continue;
86+
}
87+
$this->css_rules[ "$rules_group $selector" ] = $rule;
88+
continue;
89+
}
90+
91+
// If the selector already exists, add the declarations to it.
7492
if ( isset( $this->css_rules[ $selector ] ) ) {
7593
$this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() );
7694
continue;
@@ -117,6 +135,7 @@ public function get_css( $options = array() ) {
117135
// Build the CSS.
118136
$css = '';
119137
foreach ( $this->css_rules as $rule ) {
138+
// See class WP_Style_Engine_CSS_Rule for the get_css method.
120139
$css .= $rule->get_css( $options['prettify'] );
121140
$css .= $options['prettify'] ? "\n" : '';
122141
}

src/wp-includes/style-engine/class-wp-style-engine.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,19 +364,22 @@ protected static function is_valid_style_value( $style_value ) {
364364
* Stores a CSS rule using the provided CSS selector and CSS declarations.
365365
*
366366
* @since 6.1.0
367+
* @since 6.6.0 Added the `$rules_group` parameter.
367368
*
368369
* @param string $store_name A valid store key.
369370
* @param string $css_selector When a selector is passed, the function will return
370371
* a full CSS rule `$selector { ...rules }`
371372
* otherwise a concatenated string of properties and values.
372373
* @param string[] $css_declarations An associative array of CSS definitions,
373374
* e.g. `array( "$property" => "$value", "$property" => "$value" )`.
375+
* @param string $rules_group Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
376+
* such as `@media (min-width: 80rem)` or `@layer module`.
374377
*/
375-
public static function store_css_rule( $store_name, $css_selector, $css_declarations ) {
378+
public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) {
376379
if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) {
377380
return;
378381
}
379-
static::get_store( $store_name )->add_rule( $css_selector )->add_declarations( $css_declarations );
382+
static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations );
380383
}
381384

382385
/**

tests/phpunit/tests/style-engine/styleEngine.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,68 @@ public function test_should_dedupe_and_merge_css_rules() {
749749

750750
$this->assertSame( '.gandalf{color:white;height:190px;border-style:dotted;padding:10px;margin-bottom:100px;}.dumbledore{color:grey;height:90px;border-style:dotted;}.rincewind{color:grey;height:90px;border-style:dotted;}', $compiled_stylesheet );
751751
}
752+
753+
/**
754+
* Tests returning a generated stylesheet from a set of nested rules and merging their declarations.
755+
*
756+
* @ticket 61099
757+
*
758+
* @covers ::wp_style_engine_get_stylesheet_from_css_rules
759+
*/
760+
public function test_should_merge_declarations_for_rules_groups() {
761+
$css_rules = array(
762+
array(
763+
'selector' => '.saruman',
764+
'rules_group' => '@container (min-width: 700px)',
765+
'declarations' => array(
766+
'color' => 'white',
767+
'height' => '100px',
768+
'border-style' => 'solid',
769+
'align-self' => 'stretch',
770+
),
771+
),
772+
array(
773+
'selector' => '.saruman',
774+
'rules_group' => '@container (min-width: 700px)',
775+
'declarations' => array(
776+
'color' => 'black',
777+
'font-family' => 'The-Great-Eye',
778+
),
779+
),
780+
);
781+
782+
$compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) );
783+
784+
$this->assertSame( '@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}', $compiled_stylesheet );
785+
}
786+
787+
/**
788+
* Tests returning a generated stylesheet from a set of nested rules.
789+
*
790+
* @ticket 61099
791+
*
792+
* @covers ::wp_style_engine_get_stylesheet_from_css_rules
793+
*/
794+
public function test_should_return_stylesheet_with_nested_rules() {
795+
$css_rules = array(
796+
array(
797+
'rules_group' => '.foo',
798+
'selector' => '@media (orientation: landscape)',
799+
'declarations' => array(
800+
'background-color' => 'blue',
801+
),
802+
),
803+
array(
804+
'rules_group' => '.foo',
805+
'selector' => '@media (min-width > 1024px)',
806+
'declarations' => array(
807+
'background-color' => 'cotton-blue',
808+
),
809+
),
810+
);
811+
812+
$compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) );
813+
814+
$this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet );
815+
}
752816
}

tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ public function test_should_instantiate_with_selector_and_rules() {
3737
$this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' );
3838
}
3939

40+
/**
41+
* Tests setting and getting a rules group.
42+
*
43+
* @ticket 61099
44+
*
45+
* @covers ::set_rules_group
46+
* @covers ::get_rules_group
47+
*/
48+
public function test_should_set_rules_group() {
49+
$rule = new WP_Style_Engine_CSS_Rule( '.heres-johnny', array(), '@layer state' );
50+
51+
$this->assertSame( '@layer state', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to constructor.' );
52+
53+
$rule->set_rules_group( '@layer pony' );
54+
55+
$this->assertSame( '@layer pony', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to set_rules_group().' );
56+
}
57+
4058
/**
4159
* Tests that declaration properties are deduplicated.
4260
*

tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,22 @@ public function test_should_get_all_rule_objects_for_a_store() {
187187

188188
$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.' );
189189
}
190+
191+
/**
192+
* Tests adding rules group keys to store.
193+
*
194+
* @ticket 61099
195+
*
196+
* @covers ::add_rule
197+
*/
198+
public function test_should_store_as_concatenated_rules_groups_and_selector() {
199+
$store_one = WP_Style_Engine_CSS_Rules_Store::get_store( 'one' );
200+
$store_one_rule = $store_one->add_rule( '.tony', '.one' );
201+
202+
$this->assertSame(
203+
'.one .tony',
204+
"{$store_one_rule->get_rules_group()} {$store_one_rule->get_selector()}",
205+
'add_rule() does not concatenate rules group and selector.'
206+
);
207+
}
190208
}

0 commit comments

Comments
 (0)