Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Style Engine support for nested CSS rules. #6460

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/wp-includes/style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,14 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) {
* .elephant-are-cool{color:gray;width:3em}
*
* @since 6.1.0
* @since 6.6.0 Added support for `$rules_group` in the `$css_rules` array.
*
* @param array $css_rules {
* Required. A collection of CSS rules.
*
* @type array ...$0 {
* @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" )`.
Expand Down Expand Up @@ -154,11 +157,12 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a
continue;
}

$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'] );
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'] );
$css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group );
}

if ( empty( $css_rule_objects ) ) {
Expand Down
64 changes: 57 additions & 7 deletions src/wp-includes/style-engine/class-wp-style-engine-css-rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,33 @@ class WP_Style_Engine_CSS_Rule {
*/
protected $declarations;

/**
* A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
* such as `@media (min-width: 80rem)` or `@layer module`.
*
* @since 6.6.0
* @var string
*/
protected $rules_group;

/**
* Constructor.
*
* @since 6.1.0
* @since 6.6.0 Added the `$rules_group` parameter.
*
* @param string $selector Optional. The CSS selector. Default empty string.
* @param string[]|WP_Style_Engine_CSS_Declarations $declarations Optional. An associative array of CSS definitions,
* e.g. `array( "$property" => "$value", "$property" => "$value" )`,
* or a WP_Style_Engine_CSS_Declarations object.
* Default empty array.
* @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() ) {
public function __construct( $selector = '', $declarations = array(), $rules_group = '' ) {
$this->set_selector( $selector );
$this->add_declarations( $declarations );
$this->set_rules_group( $rules_group );
}

/**
Expand Down Expand Up @@ -89,6 +102,31 @@ public function add_declarations( $declarations ) {
return $this;
}

/**
* Sets the rules group.
*
* @since 6.6.0
*
* @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_rules_group( $rules_group ) {
$this->rules_group = $rules_group;
return $this;
}

/**
* Gets the rules group.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should new methods and updates to other public methods/APIs get a @since annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point, yeah they should!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated ✅

* @since 6.6.0
*
* @return string
*/
public function get_rules_group() {
return $this->rules_group;
}

/**
* Gets the declarations object.
*
Expand All @@ -115,6 +153,7 @@ public function get_selector() {
* Gets the CSS.
*
* @since 6.1.0
* @since 6.6.0 Added support for nested CSS with rules groups.
*
* @param bool $should_prettify Optional. Whether to add spacing, new lines and indents.
* Default false.
Expand All @@ -123,17 +162,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 ? ' ' : '';
$selector = $should_prettify ? str_replace( ',', ",\n", $this->get_selector() ) : $this->get_selector();
$css_declarations = $this->declarations->get_declarations_string( $should_prettify, $declarations_indent );
$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;
$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_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;
}

return "{$rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$rule_indent}}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,30 @@ public function get_all_rules() {
* If the rule does not exist, it will be created.
*
* @since 6.1.0
* @since 6.6.0 Added the $rules_group parameter.
*
* @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 void if the selector is empty.
*/
public function add_rule( $selector ) {
$selector = trim( $selector );
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( $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[ "$rules_group $selector" ];
}

// Create the rule if it doesn't exist.
if ( empty( $this->rules[ $selector ] ) ) {
$this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector );
Expand Down
21 changes: 20 additions & 1 deletion src/wp-includes/style-engine/class-wp-style-engine-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function add_store( $store ) {
* Adds rules to be processed.
*
* @since 6.1.0
* @since 6.6.0 Added support for rules_group.
*
* @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
Expand All @@ -70,7 +71,24 @@ public function add_rules( $css_rules ) {
}

foreach ( $css_rules as $rule ) {
$selector = $rule->get_selector();
$selector = $rule->get_selector();
$rules_group = $rule->get_rules_group();

/**
* If there is a rules_group and it already exists in the css_rules array,
* add the rule to it.
* Otherwise, create a new entry for the rules_group.
*/
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[ "$rules_group $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;
Expand Down Expand Up @@ -117,6 +135,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" : '';
}
Expand Down
7 changes: 5 additions & 2 deletions src/wp-includes/style-engine/class-wp-style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,19 +364,22 @@ protected static function is_valid_style_value( $style_value ) {
* Stores a CSS rule using the provided CSS selector and CSS declarations.
*
* @since 6.1.0
* @since 6.6.0 Added the `$rules_group` parameter.
*
* @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`.
*/
public static function store_css_rule( $store_name, $css_selector, $css_declarations ) {
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 )->add_declarations( $css_declarations );
static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations );
}

/**
Expand Down
64 changes: 64 additions & 0 deletions tests/phpunit/tests/style-engine/styleEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,68 @@ public function test_should_dedupe_and_merge_css_rules() {

$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 );
}

/**
* Tests returning a generated stylesheet from a set of nested rules and merging their declarations.
*
* @ticket 61099
*
* @covers ::wp_style_engine_get_stylesheet_from_css_rules
*/
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 = wp_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.
*
* @ticket 61099
*
* @covers ::wp_style_engine_get_stylesheet_from_css_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 = wp_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 );
}
}
18 changes: 18 additions & 0 deletions tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ 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.
*
* @ticket 61099
*
* @covers ::set_rules_group
* @covers ::get_rules_group
*/
public function test_should_set_rules_group() {
$rule = new WP_Style_Engine_CSS_Rule( '.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.
*
Expand Down
18 changes: 18 additions & 0 deletions tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,22 @@ 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.
*
* @ticket 61099
*
* @covers ::add_rule
*/
public function test_should_store_as_concatenated_rules_groups_and_selector() {
$store_one = WP_Style_Engine_CSS_Rules_Store::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.'
);
}
}
Loading
Loading