From 3f58ed5d5becedbf1f406710b56ba014e87a79de Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 2 Aug 2022 13:16:06 +1000 Subject: [PATCH] Style engine: enqueue block support styles (#42452) * Enqueuing block support styles version 1. * Linter, this one's for you. * Post trunk merge cleanup and update tests. * Removed spacing around curly braces in CSS rules. Updated tests. We could maybe add a prettify option down the road. Juggling methods around to cater for adding styles to (any) store. Also making return values consistent. * Splitting `wp_style_engine_enqueue_block_supports_styles` and `wp_style_engine_get_block_supports_styles` so we can enqueue styles that don't need parsing, e.g., layout * Integrate the processor class * Migrate layout styles to style engine store. * Update packages/style-engine/class-wp-style-engine.php Co-authored-by: Ari Stathopoulos * Tweaks for #42452 (#42691) * abstract stores * "else" not needed * compile_classnames method not needed * we have a method to get the store here * Make the wp_style_engine_add_to_store function always return a store * wp_style_engine_get_stylesheet - always return string * Merged with base branch. Added test for new method. Co-authored-by: ramonjd Co-authored-by: Ramon * Adding check for the context argument. Adding tests. * Updating the processor so that it's ignorant of stores. Why? So that it can be used to process any CSS and not just stored CSS. Updating layout for backwards compatibility in gutenberg_get_layout_style (returning the styles that are collected in the function body only) Created a new mode for incoming $css_rules to be a collection of selector + css_declaration keys. Removed wp_style_engine_get_stylesheet (from store) since we don't use it yet Added a new function wp_style_engine_get_stylesheet_from_css_rules() that will process and compile a collection of CSS rules, and not store them. * dump var_dump() * Improve the processor * remove trailing commas - compatibility with PHP < 7.2 * rename css_declarations to declarations * remove unused variable Removing unused function wp_style_engine_get_stylesheet_from_store Updating tests to check for merging and deduping * Switch parse_block_styles from public to protected static * Now that all methods are static, there's no need to call `get_instance()` * Revert get_instance() in wp_style_engine_add_to_store because we want to ensure the hook is enqueued here. * Adding a test for the 'enqueue' flag. * Update lib/block-supports/layout.php Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> * Adding a test for the 'enqueue' flag. * Add named stores to the processor * avoid setting var for something that only gets used once * Only use "else" if absolutely necessary * Add a set_name method * combine & simplify conditions * use empty() instead of isset() checks here * shorten it Co-authored-by: Ari Stathopoulos Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- lib/block-supports/elements.php | 20 +- lib/block-supports/layout.php | 120 ++++-- .../class-wp-style-engine-css-rule.php | 2 +- .../class-wp-style-engine-css-rules-store.php | 49 +++ .../class-wp-style-engine-processor.php | 73 +++- .../style-engine/class-wp-style-engine.php | 395 +++++++++++++----- ...s-wp-style-engine-css-rules-store-test.php | 42 ++ .../class-wp-style-engine-processor-test.php | 100 +++-- .../phpunit/class-wp-style-engine-test.php | 195 ++++++++- 9 files changed, 786 insertions(+), 210 deletions(-) diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index b0328c6a5f227c..2a512eab82ca96 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -104,18 +104,14 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { $class_name = gutenberg_get_elements_class_name( $block ); $link_block_styles = isset( $element_block_styles['link'] ) ? $element_block_styles['link'] : null; - if ( $link_block_styles ) { - $styles = gutenberg_style_engine_get_styles( - $link_block_styles, - array( - 'selector' => ".$class_name a", - ) - ); - - if ( ! empty( $styles['css'] ) ) { - gutenberg_enqueue_block_support_styles( $styles['css'] ); - } - } + gutenberg_style_engine_get_styles( + $link_block_styles, + array( + 'selector' => ".$class_name a", + 'context' => 'block-supports', + 'enqueue' => true, + ) + ); return null; } diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 43f0e9beffba24..8ae0ffc007fa98 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -39,9 +39,8 @@ function gutenberg_register_layout_support( $block_type ) { * @return string CSS style. */ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null, $should_skip_gap_serialization = false, $fallback_gap_value = '0.5em', $block_spacing = null ) { - $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default'; - - $style = ''; + $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default'; + $layout_styles = array(); if ( 'default' === $layout_type ) { $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : ''; $wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : ''; @@ -55,14 +54,25 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support $wide_max_width_value = wp_strip_all_tags( explode( ';', $wide_max_width_value )[0] ); if ( $content_size || $wide_size ) { - $style = "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {"; - $style .= 'max-width: ' . esc_html( $all_max_width_value ) . ';'; - $style .= 'margin-left: auto !important;'; - $style .= 'margin-right: auto !important;'; - $style .= '}'; - - $style .= "$selector > .alignwide { max-width: " . esc_html( $wide_max_width_value ) . ';}'; - $style .= "$selector .alignfull { max-width: none; }"; + array_push( + $layout_styles, + array( + 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))", + 'declarations' => array( + 'max-width' => $all_max_width_value, + 'margin-left' => 'auto !important', + 'margin-right' => 'auto !important', + ), + ), + array( + 'selector' => "$selector > .alignwide", + 'declarations' => array( 'max-width' => $wide_max_width_value ), + ), + array( + 'selector' => "$selector .alignfull", + 'declarations' => array( 'max-width' => 'none' ), + ) + ); if ( isset( $block_spacing ) ) { $block_spacing_values = gutenberg_style_engine_get_styles( @@ -74,12 +84,18 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support // Handle negative margins for alignfull children of blocks with custom padding set. // They're added separately because padding might only be set on one side. if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) { - $padding_right = $block_spacing_values['declarations']['padding-right']; - $style .= "$selector > .alignfull { margin-right:calc($padding_right * -1); }"; + $padding_right = $block_spacing_values['declarations']['padding-right']; + $layout_styles[] = array( + 'selector' => "$selector > .alignfull", + 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ), + ); } if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) { - $padding_left = $block_spacing_values['declarations']['padding-left']; - $style .= "$selector > .alignfull { margin-left: calc($padding_left * -1); }"; + $padding_left = $block_spacing_values['declarations']['padding-left']; + $layout_styles[] = array( + 'selector' => "$selector > .alignfull", + 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ), + ); } } } @@ -89,8 +105,23 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null; } if ( $gap_value && ! $should_skip_gap_serialization ) { - $style .= "$selector > * { margin-block-start: 0; margin-block-end: 0; }"; - $style .= "$selector > * + * { margin-block-start: $gap_value; margin-block-end: 0; }"; + array_push( + $layout_styles, + array( + 'selector' => "$selector > *", + 'declarations' => array( + 'margin-block-start' => '0', + 'margin-block-end' => '0', + ), + ), + array( + 'selector' => "$selector > * + *", + 'declarations' => array( + 'margin-block-start' => $gap_value, + 'margin-block-end' => '0', + ), + ) + ); } } } elseif ( 'flex' === $layout_type ) { @@ -113,7 +144,10 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support } if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) { - $style .= "$selector { flex-wrap: nowrap; }"; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'flex-wrap' => 'nowrap' ), + ); } if ( $has_block_gap_support ) { @@ -123,9 +157,10 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support $gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; } if ( $gap_value && ! $should_skip_gap_serialization ) { - $style .= "$selector {"; - $style .= "gap: $gap_value;"; - $style .= '}'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'gap' => $gap_value ), + ); } } @@ -136,29 +171,47 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support * by custom css. */ if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { - $style .= "$selector {"; - $style .= "justify-content: {$justify_content_options[ $layout['justifyContent'] ]};"; - $style .= '}'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ), + ); } if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) { - $style .= "$selector {"; - $style .= "align-items: {$vertical_alignment_options[ $layout['verticalAlignment'] ]};"; - $style .= '}'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ), + ); } } else { - $style .= "$selector {"; - $style .= 'flex-direction: column;'; + $layout_styles[ $selector ] = array( + 'flex-direction' => 'column', + 'align-items' => 'flex-start', + ); if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { - $style .= "align-items: {$justify_content_options[ $layout['justifyContent'] ]};"; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ), + ); } else { - $style .= 'align-items: flex-start;'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'align-items' => 'flex-start' ), + ); } - $style .= '}'; } } - return $style; + if ( ! empty( $layout_styles ) ) { + // Add to the style engine store to enqueue and render layout styles. + gutenberg_style_engine_add_to_store( 'layout-block-supports', $layout_styles ); + + // Return compiled layout styles to retain backwards compatibility. + // Since https://github.com/WordPress/gutenberg/pull/42452 we no longer call wp_enqueue_block_support_styles in this block supports file. + return gutenberg_style_engine_get_stylesheet_from_css_rules( $layout_styles ); + } + + return ''; } /** @@ -253,7 +306,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { // Only add container class and enqueue block support styles if unique styles were generated. if ( ! empty( $style ) ) { $class_names[] = $container_class; - wp_enqueue_block_support_styles( $style ); } } 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 9e77420f0577c2..5429b282f328db 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -51,7 +51,7 @@ public function __construct( $selector = '', $declarations = array() ) { * * @param string $selector The CSS selector. * - * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. + * @return WP_Style_Engine_CSS_Rule|void Returns the object to allow chaining of methods. */ public function set_selector( $selector ) { if ( empty( $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 1dc4da265accc7..d3cc079afe6e96 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 @@ -27,6 +27,14 @@ class WP_Style_Engine_CSS_Rules_Store { */ protected static $stores = array(); + + /** + * The store name. + * + * @var string + */ + protected $name = ''; + /** * An array of CSS Rules objects assigned to the store. * @@ -44,9 +52,50 @@ class WP_Style_Engine_CSS_Rules_Store { public static function get_store( $store_name = 'default' ) { if ( ! isset( static::$stores[ $store_name ] ) ) { static::$stores[ $store_name ] = new static(); + // Set the store name. + static::$stores[ $store_name ]->set_name( $store_name ); } return static::$stores[ $store_name ]; } + + /** + * Get an array of all available stores. + * + * @return WP_Style_Engine_CSS_Rules_Store[] + */ + public static function get_stores() { + return static::$stores; + } + + /** + * Clears all stores from static::$stores. + * + * @return void + */ + public static function remove_all_stores() { + static::$stores = array(); + } + + /** + * Set the store name. + * + * @param string $name The store name. + * + * @return void + */ + public function set_name( $name ) { + $this->name = $name; + } + + /** + * Get the store name. + * + * @return string + */ + public function get_name() { + return $this->name; + } + /** * Get an array of all rules. * diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index 646ea43e7261a8..088513e83284d1 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -2,7 +2,7 @@ /** * WP_Style_Engine_Processor * - * Compiles styles from a store of CSS rules. + * Compiles styles from stores or collection of CSS rules. * * @package Gutenberg */ @@ -12,26 +12,52 @@ } /** - * Compiles styles from a store of CSS rules. + * Compiles styles from stores or collection of CSS rules. * * @access private */ class WP_Style_Engine_Processor { /** - * The Style-Engine Store object. + * The Style-Engine Store objects * - * @var WP_Style_Engine_CSS_Rules_Store + * @var WP_Style_Engine_CSS_Rules_Store[] */ - protected $store; + protected $stores = array(); /** - * Constructor. + * The set of CSS rules that this processor will work on. * - * @param WP_Style_Engine_CSS_Rules_Store $store The store to render. + * @var WP_Style_Engine_CSS_Rule[] */ - public function __construct( WP_Style_Engine_CSS_Rules_Store $store ) { - $this->store = $store; + protected $css_rules = array(); + + /** + * Add a store to the processor. + * + * @param WP_Style_Engine_CSS_Rules_Store $store The store to add. + */ + public function add_store( WP_Style_Engine_CSS_Rules_Store $store ) { + $this->stores[ $store->get_name() ] = $store; + } + + /** + * Adds rules to be processed. + * + * @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 from a store or otherwise. + */ + public function add_rules( $css_rules ) { + if ( ! is_array( $css_rules ) ) { + $css_rules = array( $css_rules ); + } + foreach ( $css_rules as $rule ) { + $selector = $rule->get_selector(); + if ( isset( $this->css_rules[ $selector ] ) ) { + $this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() ); + continue; + } + $this->css_rules[ $rule->get_selector() ] = $rule; + } } /** @@ -40,13 +66,17 @@ public function __construct( WP_Style_Engine_CSS_Rules_Store $store ) { * @return string The computed CSS. */ public function get_css() { + // If we have stores, get the rules from them. + foreach ( $this->stores as $store ) { + $this->add_rules( $store->get_all_rules() ); + } + // Combine CSS selectors that have identical declarations. $this->combine_rules_selectors(); // Build the CSS. - $css = ''; - $rules = $this->store->get_all_rules(); - foreach ( $rules as $rule ) { + $css = ''; + foreach ( $this->css_rules as $rule ) { $css .= $rule->get_css(); } return $css; @@ -58,14 +88,12 @@ public function get_css() { * @return void */ private function combine_rules_selectors() { - $rules = $this->store->get_all_rules(); - // Build an array of selectors along with the JSON-ified styles to make comparisons easier. $selectors_json = array(); - foreach ( $rules as $selector => $rule ) { + foreach ( $this->css_rules as $rule ) { $declarations = $rule->get_declarations()->get_declarations(); ksort( $declarations ); - $selectors_json[ $selector ] = json_encode( $declarations ); + $selectors_json[ $rule->get_selector() ] = wp_json_encode( $declarations ); } // Combine selectors that have the same styles. @@ -76,18 +104,17 @@ private function combine_rules_selectors() { if ( 1 >= count( $duplicates ) ) { continue; } + + $declarations = $this->css_rules[ $selector ]->get_declarations(); + foreach ( $duplicates as $key ) { // Unset the duplicates from the $selectors_json array to avoid looping through them as well. unset( $selectors_json[ $key ] ); - // Remove the rules from the store. - $this->store->remove_rule( $key ); + // Remove the rules from the rules collection. + unset( $this->css_rules[ $key ] ); } // Create a new rule with the combined selectors. - $new_rule = $this->store->add_rule( implode( ',', $duplicates ) ); - // Set the declarations. The extra check is in place because `add_rule` in the store can return `null`. - if ( $new_rule ) { - $new_rule->add_declarations( $rules[ $selector ]->get_declarations() ); - } + $this->css_rules[ implode( ',', $duplicates ) ] = new WP_Style_Engine_CSS_Rule( implode( ',', $duplicates ), $declarations ); } } } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index c2d778066ca071..33533fc872f699 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -222,22 +222,7 @@ class WP_Style_Engine { ); /** - * Utility method to retrieve the main instance of the class. - * - * The instance will be created if it does not exist yet. - * - * @return WP_Style_Engine The main instance. - */ - public static function get_instance() { - if ( null === static::$instance ) { - static::$instance = new static(); - } - - return static::$instance; - } - - /** - * Extracts the slug in kebab case from a preset string, e.g., "heavenly-blue" from 'var:preset|color|heavenlyBlue'. + * Util: Extracts the slug in kebab case from a preset string, e.g., "heavenly-blue" from 'var:preset|color|heavenlyBlue'. * * @param string? $style_value A single css preset value. * @param string $property_key The CSS property that is the second element of the preset string. Used for matching. @@ -253,7 +238,7 @@ protected static function get_slug_from_preset_value( $style_value, $property_ke } /** - * Generates a css var string, eg var(--wp--preset--color--background) from a preset string, eg. `var:preset|space|50`. + * Util: Generates a css var string, eg var(--wp--preset--color--background) from a preset string, eg. `var:preset|space|50`. * * @param string $style_value A single css preset value. * @param array $css_vars The css var patterns used to generate the var string. @@ -275,7 +260,7 @@ protected static function get_css_var_value( $style_value, $css_vars ) { } /** - * Checks whether an incoming block style value is valid. + * Util: Checks whether an incoming block style value is valid. * * @param string? $style_value A single css preset value. * @@ -293,6 +278,150 @@ protected static function is_valid_style_value( $style_value ) { return true; } + /** + * Private constructor to prevent instantiation. + */ + private function __construct() { + // Register the hook callback to render stored styles to the page. + static::register_actions( array( __CLASS__, 'process_and_enqueue_stored_styles' ) ); + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @return WP_Style_Engine The main instance. + */ + public static function get_instance() { + if ( null === static::$instance ) { + static::$instance = new static(); + } + + return static::$instance; + } + + /** + * Stores a CSS rule using the provide CSS selector and CSS declarations. + * + * @param string $store_key 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 array $css_declarations An array of parsed CSS property => CSS value pairs. + * + * @return void. + */ + public static function store_css_rule( $store_key, $css_selector, $css_declarations ) { + if ( empty( $store_key ) || empty( $css_selector ) || empty( $css_declarations ) ) { + return; + } + static::get_store( $store_key )->add_rule( $css_selector )->add_declarations( $css_declarations ); + } + + /** + * Returns a store by store key. + * + * @param string $store_key A store key. + * + * @return WP_Style_Engine_CSS_Rules_Store + */ + public static function get_store( $store_key ) { + return WP_Style_Engine_CSS_Rules_Store::get_store( $store_key ); + } + + /** + * Taken from gutenberg_enqueue_block_support_styles() + * + * This function takes care of registering hooks to add inline styles + * in the proper place, depending on the theme in use. + * + * For block themes, it's loaded in the head. + * For classic ones, it's loaded in the body + * because the wp_head action happens before + * the render_block. + * + * @param callable $callable A user-defined callback function for a WordPress hook. + * @param int $priority To set the priority for the add_action. + * + * @see gutenberg_enqueue_block_support_styles() + */ + protected static function register_actions( $callable, $priority = 10 ) { + if ( ! $callable ) { + return; + } + add_action( 'wp_enqueue_scripts', $callable ); + add_action( + wp_is_block_theme() ? 'wp_head' : 'wp_footer', + $callable, + $priority + ); + } + + /** + * Fetches, processes and compiles stored styles, then renders them to the page. + */ + public static function process_and_enqueue_stored_styles() { + $stores = WP_Style_Engine_CSS_Rules_Store::get_stores(); + + foreach ( $stores as $key => $store ) { + $processor = new WP_Style_Engine_Processor(); + $processor->add_store( $store ); + $styles = $processor->get_css(); + + if ( ! empty( $styles ) ) { + wp_register_style( $key, false, array(), true, true ); + wp_add_inline_style( $key, $styles ); + wp_enqueue_style( $key ); + } + } + } + + /** + * Returns classnames and CSS based on the values in a styles object. + * Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. + * + * @param array $block_styles The style object. + * @param array $options array( + * 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. + * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. + * );. + * + * @return array array( + * 'declarations' => (array) An array of parsed CSS property => CSS value pairs. + * 'classnames' => (array) A flat array of classnames. + * ); + */ + public static function parse_block_styles( $block_styles, $options ) { + if ( empty( $block_styles ) || ! is_array( $block_styles ) ) { + return array(); + } + + $css_declarations = array(); + $classnames = array(); + $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; + + // Collect CSS and classnames. + foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) { + if ( empty( $block_styles[ $definition_group_key ] ) ) { + continue; + } + foreach ( $definition_group_style as $style_definition ) { + $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); + + if ( ! static::is_valid_style_value( $style_value ) ) { + continue; + } + + $classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) ); + $css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $should_skip_css_vars ) ); + } + } + + return array( + 'classnames' => $classnames, + 'declarations' => $css_declarations, + ); + } + /** * Returns classnames, and generates classname(s) from a CSS preset property pattern, e.g., 'var:preset|color|heavenly-blue'. * @@ -339,10 +468,7 @@ protected static function get_classnames( $style_value, $style_definition ) { * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). */ protected static function get_css_declarations( $style_value, $style_definition, $should_skip_css_vars = false ) { - if ( - isset( $style_definition['value_func'] ) && - is_callable( $style_definition['value_func'] ) - ) { + if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $should_skip_css_vars ); } @@ -369,7 +495,9 @@ protected static function get_css_declarations( $style_value, $style_definition, if ( is_string( $value ) && strpos( $value, 'var:' ) !== false && ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) { $value = static::get_css_var_value( $value, $style_definition['css_vars'] ); } + $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); + if ( $individual_property && static::is_valid_style_value( $value ) ) { $css_declarations[ $individual_property ] = $value; } @@ -381,73 +509,6 @@ protected static function get_css_declarations( $style_value, $style_definition, return $css_declarations; } - /** - * Returns classnames and CSS based on the values in a styles object. - * Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. - * - * @param array $block_styles The style object. - * @param array $options array( - * 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. - * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. - * );. - * - * @return array|null array( - * 'css' => (string) A CSS ruleset formatted to be placed in an HTML `style` attribute or tag. Default is a string of inline styles. - * 'classnames' => (string) Classnames separated by a space. - * ); - */ - public function get_styles( $block_styles, $options ) { - if ( empty( $block_styles ) || ! is_array( $block_styles ) ) { - return null; - } - - $css_declarations = array(); - $classnames = array(); - $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; - - // Collect CSS and classnames. - foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) { - if ( empty( $block_styles[ $definition_group_key ] ) ) { - continue; - } - foreach ( $definition_group_style as $style_definition ) { - $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); - - if ( ! static::is_valid_style_value( $style_value ) ) { - continue; - } - - $classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) ); - $css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $should_skip_css_vars ) ); - } - } - - // Build CSS rules output. - $css_selector = isset( $options['selector'] ) ? $options['selector'] : null; - $css_declarations = new WP_Style_Engine_CSS_Declarations( $css_declarations ); - - // The return object. - $styles_output = array(); - $css = $css_declarations->get_declarations_string(); - - // Return css, if any. - if ( ! empty( $css ) ) { - $styles_output['css'] = $css; - $styles_output['declarations'] = $css_declarations->get_declarations(); - // Return an entire rule if there is a selector. - if ( $css_selector ) { - $styles_output['css'] = $css_selector . ' { ' . $css . ' }'; - } - } - - // Return classnames, if any. - if ( ! empty( $classnames ) ) { - $styles_output['classnames'] = implode( ' ', array_unique( $classnames ) ); - } - - return $styles_output; - } - /** * Style value parser that returns a CSS definition array comprising style properties * that have keys representing individual style properties, otherwise known as longhand CSS properties. @@ -494,10 +555,45 @@ protected static function get_individual_property_css_declarations( $style_value } return $css_declarations; } + + /** + * Returns compiled CSS from css_declarations. + * + * @param array $css_declarations An array of parsed CSS property => CSS value pairs. + * @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. + * + * @return string A compiled CSS string. + */ + public static function compile_css( $css_declarations, $css_selector ) { + if ( empty( $css_declarations ) || ! is_array( $css_declarations ) ) { + return ''; + } + + // Return an entire rule if there is a selector. + if ( $css_selector ) { + $css_rule = new WP_Style_Engine_CSS_Rule( $css_selector, $css_declarations ); + return $css_rule->get_css(); + } + $css_declarations = new WP_Style_Engine_CSS_Declarations( $css_declarations ); + return $css_declarations->get_declarations_string(); + } + + /** + * Returns a compiled stylesheet from stored CSS rules. + * + * @param WP_Style_Engine_CSS_Rule[] $css_rules An array of WP_Style_Engine_CSS_Rule objects from a store or otherwise. + * + * @return string A compiled stylesheet from stored CSS rules. + */ + public static function compile_stylesheet_from_css_rules( $css_rules ) { + $processor = new WP_Style_Engine_Processor(); + $processor->add_rules( $css_rules ); + return $processor->get_css(); + } } /** - * Global public interface method to WP_Style_Engine->get_styles to generate styles from a single style object, e.g., + * Global public interface method to generate styles from a single style object, e.g., * the value of a block's attributes.style object or the top level styles in theme.json. * See: https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/#styles and * https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/ @@ -511,21 +607,124 @@ protected static function get_individual_property_css_declarations( $style_value * * @param array $block_styles The style object. * @param array $options array( - * 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. * 'context' => (string) An identifier describing the origin of the style object, e.g., 'block-supports' or 'global-styles'. Default is 'block-supports'. + * 'enqueue' => (boolean) When `true` will attempt to store and enqueue for rendering on the frontend. * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. + * 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. * );. * * @return array|null array( - * 'css' => (string) A CSS ruleset or declarations block formatted to be placed in an HTML `style` attribute or tag. - * 'declarations' => (array) An array of property/value pairs representing parsed CSS declarations. - * 'classnames' => (string) Classnames separated by a space. + * 'css' => (string) A CSS ruleset or declarations block formatted to be placed in an HTML `style` attribute or tag. + * 'declarations' => (array) An array of property/value pairs representing parsed CSS declarations. + * 'classnames' => (string) Classnames separated by a space. * ); */ function wp_style_engine_get_styles( $block_styles, $options = array() ) { - if ( class_exists( 'WP_Style_Engine' ) ) { - $style_engine = WP_Style_Engine::get_instance(); - return $style_engine->get_styles( $block_styles, $options ); + if ( ! class_exists( 'WP_Style_Engine' ) ) { + return array(); + } + $defaults = array( + 'selector' => null, + 'context' => 'block-supports', + 'convert_vars_to_classnames' => false, + 'enqueue' => false, + ); + + $style_engine = WP_Style_Engine::get_instance(); + $options = wp_parse_args( $options, $defaults ); + $parsed_styles = null; + + // Block supports styles. + if ( 'block-supports' === $options['context'] ) { + $parsed_styles = $style_engine::parse_block_styles( $block_styles, $options ); } - return null; + + // Output. + $styles_output = array(); + + if ( ! $parsed_styles ) { + return $styles_output; + } + + if ( ! empty( $parsed_styles['declarations'] ) ) { + $styles_output['css'] = $style_engine::compile_css( $parsed_styles['declarations'], $options['selector'] ); + $styles_output['declarations'] = $parsed_styles['declarations']; + if ( true === $options['enqueue'] ) { + $style_engine::store_css_rule( $options['context'], $options['selector'], $parsed_styles['declarations'] ); + } + } + + if ( ! empty( $parsed_styles['classnames'] ) ) { + $styles_output['classnames'] = implode( ' ', array_unique( $parsed_styles['classnames'] ) ); + } + + return array_filter( $styles_output ); +} + +/** + * Global public interface method to register styles to be enqueued and rendered. + * + * @access public + * + * @param string $store_key A valid store key. + * @param array $css_rules array( + * 'selector' => (string) A CSS selector. + * 'declarations' => (boolean) An array of CSS definitions, e.g., array( "$property" => "$value" ). + * );. + * + * @return WP_Style_Engine_CSS_Rules_Store|null. + */ +function wp_style_engine_add_to_store( $store_key, $css_rules = array() ) { + if ( ! class_exists( 'WP_Style_Engine' ) || ! $store_key ) { + return null; + } + + // Get instance here to ensure that we register hooks to enqueue stored styles. + $style_engine = WP_Style_Engine::get_instance(); + + if ( empty( $css_rules ) ) { + return $style_engine::get_store( $store_key ); + } + + foreach ( $css_rules as $css_rule ) { + if ( empty( $css_rule['selector'] ) || empty( $css_rule['declarations'] ) ) { + continue; + } + $style_engine::store_css_rule( $store_key, $css_rule['selector'], $css_rule['declarations'] ); + } + return $style_engine::get_store( $store_key ); +} + +/** + * Returns compiled CSS from a collection of selectors and declarations. + * This won't add to any store, but is useful for returning a compiled style sheet from any CSS selector + declarations combos. + * + * @access public + * + * @param array $css_rules array( + * 'selector' => (string) A CSS selector. + * 'declarations' => (boolean) An array of CSS definitions, e.g., array( "$property" => "$value" ). + * );. + * + * @return string A compiled CSS string. + */ +function wp_style_engine_get_stylesheet_from_css_rules( $css_rules = array() ) { + if ( ! class_exists( 'WP_Style_Engine' ) ) { + return ''; + } + + $css_rule_objects = array(); + + foreach ( $css_rules as $css_rule ) { + if ( empty( $css_rule['selector'] ) || empty( $css_rule['declarations'] ) || ! is_array( $css_rule['declarations'] ) ) { + continue; + } + $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'] ); + } + + if ( empty( $css_rule_objects ) ) { + return ''; + } + + return WP_Style_Engine::compile_stylesheet_from_css_rules( $css_rule_objects ); } diff --git a/packages/style-engine/phpunit/class-wp-style-engine-css-rules-store-test.php b/packages/style-engine/phpunit/class-wp-style-engine-css-rules-store-test.php index 84dc6b5f2e82a1..9fe664752e4bff 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-css-rules-store-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-css-rules-store-test.php @@ -14,6 +14,13 @@ * Tests for registering, storing and retrieving CSS Rules. */ class WP_Style_Engine_CSS_Rules_Store_Test extends WP_UnitTestCase { + /** + * Tear down after each test. + */ + public function tear_down() { + parent::tear_down(); + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + } /** * Should create a new store. */ @@ -36,6 +43,41 @@ public function test_get_store() { $this->assertEquals( $selector, $the_same_fish_store->add_rule( $selector )->get_selector() ); } + /** + * Should return all previously created stores. + */ + public function test_get_stores() { + $burrito_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'burrito' ); + $quesadilla_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'quesadilla' ); + $this->assertEquals( + array( + 'burrito' => $burrito_store, + 'quesadilla' => $quesadilla_store, + ), + WP_Style_Engine_CSS_Rules_Store::get_stores() + ); + } + + /** + * Should delete all previously created stores. + */ + public function test_remove_all_stores() { + $dolmades_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'dolmades' ); + $tzatziki_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'tzatziki' ); + $this->assertEquals( + array( + 'dolmades' => $dolmades_store, + 'tzatziki' => $tzatziki_store, + ), + WP_Style_Engine_CSS_Rules_Store::get_stores() + ); + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + $this->assertEquals( + array(), + WP_Style_Engine_CSS_Rules_Store::get_stores() + ); + } + /** * Should return a stored rule. */ diff --git a/packages/style-engine/phpunit/class-wp-style-engine-processor-test.php b/packages/style-engine/phpunit/class-wp-style-engine-processor-test.php index 8070024adf3133..ebee00f05ce16a 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-processor-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-processor-test.php @@ -14,12 +14,35 @@ * Tests for compiling and rendering styles from a store of CSS rules. */ class WP_Style_Engine_Processor_Test extends WP_UnitTestCase { + /** + * Should compile CSS rules. + */ + public function test_return_rules_as_css() { + $a_nice_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nice-rule' ); + $a_nice_css_rule->add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nicer-rule' ); + $a_nicer_css_rule->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nice_processor = new WP_Style_Engine_Processor(); + $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); + $this->assertEquals( '.a-nice-rule {color: var(--nice-color); background-color: purple;}.a-nicer-rule {font-family: Nice sans; font-size: 1em; background-color: purple;}', $a_nice_processor->get_css() ); + } + /** * Should compile CSS rules from the store. */ public function test_return_store_rules_as_css() { - $a_nice_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'nice' ); - $a_nice_renderer = new WP_Style_Engine_Processor_Gutenberg( $a_nice_store ); + $a_nice_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'nice' ); $a_nice_store->add_rule( '.a-nice-rule' )->add_declarations( array( 'color' => 'var(--nice-color)', @@ -33,7 +56,8 @@ public function test_return_store_rules_as_css() { 'background-color' => 'purple', ) ); - + $a_nice_renderer = new WP_Style_Engine_Processor_Gutenberg(); + $a_nice_renderer->add_store( $a_nice_store ); $this->assertEquals( '.a-nice-rule {color: var(--nice-color); background-color: purple;}.a-nicer-rule {font-family: Nice sans; font-size: 1em; background-color: purple;}', $a_nice_renderer->get_css() ); } @@ -41,87 +65,101 @@ public function test_return_store_rules_as_css() { * Should merge CSS declarations. */ public function test_dedupe_and_merge_css_declarations() { - $an_excellent_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'excellent' ); - $an_excellent_renderer = new WP_Style_Engine_Processor_Gutenberg( $an_excellent_store ); - $an_excellent_store->add_rule( '.an-excellent-rule' )->add_declarations( + $an_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $an_excellent_processor = new WP_Style_Engine_Processor(); + $an_excellent_rule->add_declarations( array( 'color' => 'var(--excellent-color)', 'border-style' => 'dotted', ) ); - $an_excellent_store->add_rule( '.an-excellent-rule' )->add_declarations( + $an_excellent_processor->add_rules( $an_excellent_rule ); + + $another_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $another_excellent_rule->add_declarations( array( 'color' => 'var(--excellent-color)', 'border-style' => 'dotted', 'border-color' => 'brown', ) ); + $an_excellent_processor->add_rules( $another_excellent_rule ); + $this->assertEquals( '.an-excellent-rule {color: var(--excellent-color); border-style: dotted; border-color: brown;}', $an_excellent_processor->get_css() ); - $this->assertEquals( '.an-excellent-rule {color: var(--excellent-color); border-style: dotted; border-color: brown;}', $an_excellent_renderer->get_css() ); - - $an_excellent_store->add_rule( '.an-excellent-rule' )->add_declarations( + $yet_another_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $yet_another_excellent_rule->add_declarations( array( 'color' => 'var(--excellent-color)', 'border-style' => 'dashed', 'border-width' => '2px', ) ); - - $this->assertEquals( '.an-excellent-rule {color: var(--excellent-color); border-style: dashed; border-color: brown; border-width: 2px;}', $an_excellent_renderer->get_css() ); + $an_excellent_processor->add_rules( $yet_another_excellent_rule ); + $this->assertEquals( '.an-excellent-rule {color: var(--excellent-color); border-style: dashed; border-color: brown; border-width: 2px;}', $an_excellent_processor->get_css() ); } /** * Should combine duplicate CSS rules. */ public function test_combine_css_rules() { - $a_sweet_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'sweet' ); - $a_sweet_renderer = new WP_Style_Engine_Processor_Gutenberg( $a_sweet_store ); - $a_sweet_store->add_rule( '.a-sweet-rule' )->add_declarations( + $a_sweet_rule = new WP_Style_Engine_CSS_Rule( + '.a-sweet-rule', array( 'color' => 'var(--sweet-color)', 'background-color' => 'purple', ) ); - $a_sweet_store->add_rule( '#an-even-sweeter-rule > marquee' )->add_declarations( + + $a_sweeter_rule = new WP_Style_Engine_CSS_Rule( + '#an-even-sweeter-rule > marquee', array( 'color' => 'var(--sweet-color)', 'background-color' => 'purple', ) ); - $this->assertEquals( '.a-sweet-rule,#an-even-sweeter-rule > marquee {color: var(--sweet-color); background-color: purple;}', $a_sweet_renderer->get_css() ); - } + $a_sweet_processor = new WP_Style_Engine_Processor(); + $a_sweet_processor->add_rules( array( $a_sweet_rule, $a_sweeter_rule ) ); - /** - * Should combine and store CSS rules. - */ - public function test_store_combined_css_rules() { - $a_lovely_store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'lovely' ); - $a_lovely_renderer = new WP_Style_Engine_Processor_Gutenberg( $a_lovely_store ); - $a_lovely_store->add_rule( '.a-lovely-rule' )->add_declarations( + $this->assertEquals( '.a-sweet-rule,#an-even-sweeter-rule > marquee {color: var(--sweet-color); background-color: purple;}', $a_sweet_processor->get_css() ); + } + /** + * Should combine and store CSS rules. + */ + public function test_combine_previously_added_css_rules() { + $a_lovely_processor = new WP_Style_Engine_Processor(); + $a_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-lovely-rule', array( 'border-color' => 'purple', ) ); - $a_lovely_store->add_rule( '.a-lovelier-rule' )->add_declarations( + $a_lovely_processor->add_rules( $a_lovely_rule ); + $a_lovelier_rule = new WP_Style_Engine_CSS_Rule( + '.a-lovelier-rule', array( 'border-color' => 'purple', ) ); + $a_lovely_processor->add_rules( $a_lovelier_rule ); + $this->assertEquals( '.a-lovely-rule,.a-lovelier-rule {border-color: purple;}', $a_lovely_processor->get_css() ); - $this->assertEquals( '.a-lovely-rule,.a-lovelier-rule {border-color: purple;}', $a_lovely_renderer->get_css() ); - - $a_lovely_store->add_rule( '.a-most-lovely-rule' )->add_declarations( + $a_most_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-most-lovely-rule', array( 'border-color' => 'purple', ) ); - $a_lovely_store->add_rule( '.a-perfectly-lovely-rule' )->add_declarations( + $a_lovely_processor->add_rules( $a_most_lovely_rule ); + + $a_perfectly_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-perfectly-lovely-rule', array( 'border-color' => 'purple', ) ); + $a_lovely_processor->add_rules( $a_perfectly_lovely_rule ); - $this->assertEquals( '.a-lovely-rule,.a-lovelier-rule,.a-most-lovely-rule,.a-perfectly-lovely-rule {border-color: purple;}', $a_lovely_renderer->get_css() ); + $this->assertEquals( '.a-lovely-rule,.a-lovelier-rule,.a-most-lovely-rule,.a-perfectly-lovely-rule {border-color: purple;}', $a_lovely_processor->get_css() ); } } diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/class-wp-style-engine-test.php index 81ca7f931f7644..14938e075fa260 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -6,44 +6,55 @@ * @subpackage style-engine */ +require __DIR__ . '/../class-wp-style-engine-processor.php'; require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +require __DIR__ . '/../class-wp-style-engine-css-rule.php'; +require __DIR__ . '/../class-wp-style-engine-css-rules-store.php'; require __DIR__ . '/../class-wp-style-engine.php'; /** * Tests for registering, storing and generating styles. */ class WP_Style_Engine_Test extends WP_UnitTestCase { + /** + * Tear down after each test. + */ + public function tear_down() { + parent::tear_down(); + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + } + /** * Tests generating block styles and classnames based on various manifestations of the $block_styles argument. * - * @dataProvider data_generate_block_supports_styles_fixtures + * @dataProvider data_get_styles_fixtures * - * @param array $block_styles The incoming block styles object. - * @param array $options Style engine options. + * @param array $block_styles The incoming block styles object. + * @param array $options Style engine options. * @param string $expected_output The expected output. */ - public function test_generate_block_supports_styles( $block_styles, $options, $expected_output ) { + public function test_generate_get_styles( $block_styles, $options, $expected_output ) { $generated_styles = wp_style_engine_get_styles( $block_styles, $options ); $this->assertSame( $expected_output, $generated_styles ); } /** - * Data provider. + * Data provider for test_generate_get_styles(). * * @return array */ - public function data_generate_block_supports_styles_fixtures() { + public function data_get_styles_fixtures() { return array( 'default_return_value' => array( 'block_styles' => array(), 'options' => null, - 'expected_output' => null, + 'expected_output' => array(), ), 'inline_invalid_block_styles_empty' => array( 'block_styles' => 'hello world!', 'options' => null, - 'expected_output' => null, + 'expected_output' => array(), ), 'inline_invalid_block_styles_unknown_style' => array( @@ -72,7 +83,7 @@ public function data_generate_block_supports_styles_fixtures() { 'expected_output' => array(), ), - 'valid_inline_css_and_classnames' => array( + 'valid_inline_css_and_classnames_as_default_context' => array( 'block_styles' => array( 'color' => array( 'text' => 'var:preset|color|texas-flood', @@ -100,6 +111,44 @@ public function data_generate_block_supports_styles_fixtures() { ), ), + 'valid_inline_css_and_classnames_with_context' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|little-lamb', + ), + 'spacing' => array( + 'margin' => '20px', + ), + ), + 'options' => array( + 'convert_vars_to_classnames' => true, + 'context' => 'block-supports', + ), + 'expected_output' => array( + 'css' => 'margin: 20px;', + 'declarations' => array( + 'margin' => '20px', + ), + 'classnames' => 'has-text-color has-little-lamb-color', + ), + ), + + 'invalid_context' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|sugar', + ), + 'spacing' => array( + 'padding' => '20000px', + ), + ), + 'options' => array( + 'convert_vars_to_classnames' => true, + 'context' => 'i-love-doughnuts', + ), + 'expected_output' => array(), + ), + 'inline_valid_box_model_style' => array( 'block_styles' => array( 'spacing' => array( @@ -187,7 +236,7 @@ public function data_generate_block_supports_styles_fixtures() { ), 'options' => array( 'selector' => '.wp-selector > p' ), 'expected_output' => array( - 'css' => '.wp-selector > p { padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; }', + 'css' => '.wp-selector > p {padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem;}', 'declarations' => array( 'padding-top' => '42px', 'padding-left' => '2%', @@ -207,7 +256,7 @@ public function data_generate_block_supports_styles_fixtures() { 'selector' => '.wp-selector', ), 'expected_output' => array( - 'css' => '.wp-selector { color: var(--wp--preset--color--my-little-pony); }', + 'css' => '.wp-selector {color: var(--wp--preset--color--my-little-pony);}', 'declarations' => array( 'color' => 'var(--wp--preset--color--my-little-pony)', ), @@ -458,4 +507,128 @@ public function data_generate_block_supports_styles_fixtures() { ), ); } + + /** + * Tests adding rules to a store and retrieving a generated stylesheet. + */ + public function test_enqueue_block_styles_store() { + $block_styles = array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ); + + $generated_styles = wp_style_engine_get_styles( + $block_styles, + array( + 'enqueue' => true, + 'context' => 'block-supports', + 'selector' => 'article', + ) + ); + $store = WP_Style_Engine::get_instance()::get_store( 'block-supports' ); + $rule = $store->get_all_rules()['article']; + $this->assertSame( $generated_styles['css'], $rule->get_css() ); + } + + /** + * Tests adding rules to a store and retrieving a generated stylesheet. + */ + public function test_add_to_store() { + $store = wp_style_engine_add_to_store( 'test-store', array() ); + + // wp_style_engine_add_to_store returns a store object. + $this->assertInstanceOf( 'WP_Style_Engine_CSS_Rules_Store', $store ); + + // Check that the style engine knows about the store. + $stored_store = WP_Style_Engine::get_instance()::get_store( 'test-store' ); + $this->assertInstanceOf( 'WP_Style_Engine_CSS_Rules_Store', $stored_store ); + } + + /** + * Tests retrieving a generated stylesheet from any rules. + */ + public function test_get_stylesheet_from_css_rules() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'unset', + ), + ), + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + 'align-self' => 'safe center', + ), + ), + array( + 'selector' => '.radagast', + 'declarations' => array( + 'color' => 'brown', + 'height' => '60px', + 'border-style' => 'dashed', + 'align-self' => 'stretch', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules ); + $this->assertSame( '.saruman {color: white; height: 100px; border-style: solid; align-self: unset;}.gandalf {color: grey; height: 90px; border-style: dotted; align-self: safe center;}.radagast {color: brown; height: 60px; border-style: dashed; align-self: stretch;}', $compiled_stylesheet ); + } + + /** + * Tests that incoming styles are deduped and merged. + */ + public function test_get_deduped_and_merged_stylesheet() { + $css_rules = array( + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'white', + 'height' => '190px', + 'padding' => '10px', + 'margin-bottom' => '100px', + ), + ), + array( + 'selector' => '.dumbledore', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + array( + 'selector' => '.rincewind', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules ); + $this->assertSame( '.gandalf {color: white; height: 190px; border-style: dotted; padding: 10px; margin-bottom: 100px;}.dumbledore,.rincewind {color: grey; height: 90px; border-style: dotted;}', $compiled_stylesheet ); + } }