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

Style engine: support nested CSS rules (or CSS containers) #58797

Closed
wants to merge 20 commits into from
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
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
62 changes: 13 additions & 49 deletions packages/style-engine/class-wp-style-engine-css-rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member Author

@ramonjd ramonjd Feb 10, 2024

Choose a reason for hiding this comment

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

Aside from this copy update, these changes just revert those in #58867

*
* @access private
*/
Expand All @@ -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 );
}

/**
Expand All @@ -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;
}

Expand All @@ -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;

Expand All @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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}}";
}
}
Expand Down
123 changes: 123 additions & 0 deletions packages/style-engine/class-wp-style-engine-css-rules-container.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php
/**
* WP_Style_Engine_CSS_Rules_Container
*
* A container for WP_Style_Engine_CSS_Rule objects.
*
* @package Gutenberg
*/

if ( ! class_exists( 'WP_Style_Engine_CSS_Rules_Container' ) ) {
Copy link
Member Author

@ramonjd ramonjd Feb 10, 2024

Choose a reason for hiding this comment

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

New class with a similar interface to WP_Style_Engine_CSS_Rule

This is intentional so that the relevant getter class methods such as get_css/selector can be called where there is a collection of rules + nested rules, e.g., in a single store.

Also so WP_Style_Engine_CSS_Rule doesn't need to care about whether its nested or not, which is the same as the declarations class, and can take care of its own output.

Copy link
Member Author

@ramonjd ramonjd Feb 10, 2024

Choose a reason for hiding this comment

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

TODO - WP_Style_Engine_CSS_Rules_Container should extend WP_Style_Engine_CSS_Rule, or even look into adding WP_Style_Engine_CSS_Rules_Container methods to WP_Style_Engine_CSS_Rule itself.

I'd say the former to maintain separation of concerns.

Reason being, with nested CSS, the container rule should also be able to have declarations.

This can be handled by the existing API, indicated by passing a container and not a selector:

wp_style_engine_get_stylesheet_from_css_rules(
    array(
          'container' => 'fieldset',
          'declarations' => array( .... )
     ),
    array(
          'container' => 'fieldset',
           'container' => 'textarea',
          'declarations' => array( .... )
     )
)

/*
  Output:

   fieldset {
       ...declarations;
      textarea {
           ...declarations;
       }
    }
*/ 

/**
* Holds, sanitizes, processes and prints nested CSS rules for the Style Engine.
*
* @access private
*/
class WP_Style_Engine_CSS_Rules_Container extends WP_Style_Engine_CSS_Rule {
/**
* The container declarations.
*
* Contains a WP_Style_Engine_CSS_Rule object.
*
* @var WP_Style_Engine_CSS_Rule[]
*/
protected $rules = array();

/**
* Constructor
*
* @param string $selector A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
* such as `@media (min-width: 80rem)` or `@layer module`.
* @param WP_Style_Engine_CSS_Rule[]|WP_Style_Engine_CSS_Rule $rule Optional. A WP_Style_Engine_CSS_Rule object.
*/
public function __construct( $selector = '', $rule = array() ) {
$this->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(
Copy link
Member Author

Choose a reason for hiding this comment

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

I can't seem to unit test this using $this->expectNotice();

__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;
Copy link
Member Author

Choose a reason for hiding this comment

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

Bumps the indent count for nested rules

$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}}";
}
}
}
35 changes: 20 additions & 15 deletions packages/style-engine/class-wp-style-engine-css-rules-store.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ];
Expand Down
Loading
Loading