diff --git a/lib/load.php b/lib/load.php index 1493b5ee79791..9792ef4870937 100644 --- a/lib/load.php +++ b/lib/load.php @@ -115,19 +115,12 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experiments-page.php'; // Copied package PHP files. -if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php' ) ) { +if ( is_dir( __DIR__ . '/../build/style-engine' ) ) { + require_once __DIR__ . '/../build/style-engine/style-engine-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php'; -} -if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-css-declarations-gutenberg.php' ) ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-declarations-gutenberg.php'; -} -if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rule-gutenberg.php' ) ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rule-gutenberg.php'; -} -if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-store-gutenberg.php' ) ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-store-gutenberg.php'; -} -if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-processor-gutenberg.php' ) ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-processor-gutenberg.php'; } diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index e1624aceb5ade..6e128fe13fd1a 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Include `@wordpress/style-engine` on the list of external dependencies to allow using `wp.styleEngine` global with WordPress 6.1 and beyond ([#43840](https://github.com/WordPress/gutenberg/pull/43840)). + ## 4.0.0 (2022-08-24) ### Breaking Change diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index 87d67f8dbd68f..a22837af6a72e 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -1,9 +1,5 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ - '@wordpress/icons', - '@wordpress/interface', - '@wordpress/style-engine', -]; +const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; /** * Default request to global transformation diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index 1206f084cdea2..2812552c624b4 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,36 +2,22 @@ ## Unreleased -## 0.16.0 (2022-09-13) - -## 0.15.0 (2022-08-24) - -## 0.14.0 (2022-08-10) - -## 0.13.0 (2022-07-27) - -## 0.12.0 (2022-07-13) - -## 0.11.0 (2022-06-29) - -## 0.10.0 (2022-06-15) - -## 0.9.0 (2022-06-01) - -## 0.8.0 (2022-05-18) - -## 0.7.0 (2022-05-04) - -## 0.6.0 (2022-04-21) - -## 0.5.0 (2022-04-08) - -## 0.4.0 (2022-03-23) - -## 0.3.0 (2022-03-11) - -## 0.2.0 (2022-02-23) - -### New Feature - +### Enhancement +- Allow for prettified output ([#42909](https://github.com/WordPress/gutenberg/pull/42909)). +- Enqueue block supports styles in Gutenberg ([#42880](https://github.com/WordPress/gutenberg/pull/42880)). + +### Internal +- Move backend scripts to package ([#39736](https://github.com/WordPress/gutenberg/pull/39736)). +- Updating docs, formatting, and separating global functions from the main class file ([#43840](https://github.com/WordPress/gutenberg/pull/43840)). + +### New Features +- Add a WP_Style_Engine_Processor object ([#42463](https://github.com/WordPress/gutenberg/pull/42463)). +- Add a WP_Style_Engine_CSS_Declarations object ([#42043](https://github.com/WordPress/gutenberg/pull/42043)). +- Add Rules and Store objects ([#42222](https://github.com/WordPress/gutenberg/pull/42222)). +- Add elements styles support ([#41732](https://github.com/WordPress/gutenberg/pull/41732)) and ([#40987](https://github.com/WordPress/gutenberg/pull/40987)). +- Add typography and color support ([#40665](https://github.com/WordPress/gutenberg/pull/40987)) and ([#40332](https://github.com/WordPress/gutenberg/pull/40332)). +- Add border support ([#41803](https://github.com/WordPress/gutenberg/pull/40332)) and ([#40531](https://github.com/WordPress/gutenberg/pull/40531)). +- Add margin support to frontend ([#39790](https://github.com/WordPress/gutenberg/pull/39790)). +- Add basic block supports to backend ([#39446](https://github.com/WordPress/gutenberg/pull/39446)). - Added initial version of the style engine ([#37978](https://github.com/WordPress/gutenberg/pull/37978)). +- Include `@wordpress/style-engine` on the list of external dependencies to allow using `wp.styleEngine` global with WordPress 6.1 and beyond ([#43840](https://github.com/WordPress/gutenberg/pull/43840)). diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md index 1bd3e35e189f2..7ef6ef36c0e04 100644 --- a/packages/style-engine/README.md +++ b/packages/style-engine/README.md @@ -1,27 +1,23 @@ # Style Engine -The Style Engine powering global styles and block customizations. +The Style Engine aims to provide a consistent API for rendering styling for blocks across both client-side and server-side applications. -## Important +Initially, it will offer a single, centralized agent responsible for generating block styles, and, in later phases, it will also assume the responsibility of processing and rendering optimized frontend CSS. -This Package is considered experimental at the moment. The idea is to have a package used to generate styles based on a -style object that is consistent between: backend, frontend, block style object and theme.json. +## Important -Because this package is experimental and still in development it does not yet generate a `wp.styleEngine` global. To get -there, the following tasks need to be completed: +This package is new as of WordPress 6.1 and therefore in its infancy. -**TODO List:** +Upcoming tasks on the roadmap include, but are not limited to, the following: -- Add style definitions for all the currently supported styles in blocks and theme.json. -- The CSS variable shortcuts for values (for presets...) -- Support generating styles in the frontend. (Ongoing) -- Support generating styles in the backend (block supports and theme.json stylesheet). (Ongoing) -- Refactor all block styles to use the style engine server side. (Ongoing) -- Consolidate global and block style rendering and enqueuing -- Refactor all blocks to consistently use the "style" attribute for all customizations (get rid of the preset specific - attributes). +- Consolidate global and block style rendering and enqueuing (ongoing) +- Explore pre-render CSS rule processing with the intention of deduplicating other common and/or repetitive block styles. (ongoing) +- Extend the scope of semantic class names and/or design token expression, and encapsulate rules into stable utility classes. +- Explore pre-render CSS rule processing with the intention of deduplicating other common and/or repetitive block styles. +- Propose a way to control hierarchy and specificity, and make the style hierarchy cascade accessible and predictable. This might include preparing for CSS cascade layers until they become more widely supported, and allowing for opt-in support in Gutenberg via theme.json. +- Refactor all blocks to consistently use the "style" attribute for all customizations, that is, deprecate preset-specific attributes such as `attributes.fontSize`. -See [Tracking: Add a Style Engine to manage rendering block styles #38167](https://github.com/WordPress/gutenberg/issues/38167) +For more information about the roadmap, please refer to [Block editor styles: initiatives and goals](https://make.wordpress.org/core/2022/06/24/block-editor-styles-initiatives-and-goals/) and the [Github project board](https://github.com/orgs/WordPress/projects/19). ## Backend API @@ -30,8 +26,9 @@ See [Tracking: Add a Style Engine to manage rendering block styles #38167](https Global public function to generate styles from a single style object, e.g., the value of a [block's attributes.style object](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/#styles) or -the [top level styles in theme.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) -. +the [top level styles in theme.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/). + +See also [Using the Style Engine to generate block supports styles](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-style-engine/using-the-style-engine-with-block-supports). _Parameters_ @@ -82,7 +79,7 @@ print_r( $styles ); /* array( - 'css' => '.a-selector{padding:10px}' + 'css' => '.a-selector{padding:100px}' 'declarations' => array( 'padding' => '100px' ) ) */ @@ -132,7 +129,7 @@ $stylesheet = wp_style_engine_get_stylesheet_from_css_rules( 'context' => 'block-supports', // Indicates that these styles should be stored with block supports CSS. ) ); -print_r( $stylesheet ); // .wp-pumpkin, .wp-kumquat {color:orange}.wp-tomato{color:red;padding:100px} +print_r( $stylesheet ); // .wp-pumpkin,.wp-kumquat{color:orange}.wp-tomato{color:red;padding:100px} ``` ### wp_style_engine_get_stylesheet_from_context() @@ -185,10 +182,7 @@ Install the module npm install @wordpress/style-engine --save ``` -_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has -limited or no support for such language features and APIs, you should -include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) -in your code._ +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ ## Usage @@ -205,7 +199,11 @@ _Parameters_ _Returns_ -- `string`: generated stylesheet. +- `string`: A generated stylesheet or inline style declarations. + +_Changelog_ + +`6.1.0` Introduced in WordPress core. ### getCSSRules @@ -218,7 +216,11 @@ _Parameters_ _Returns_ -- `GeneratedCSSRule[]`: generated styles. +- `GeneratedCSSRule[]`: A collection of objects containing the selector, if any, the CSS property key (camelcase) and parsed CSS value. + +_Changelog_ + +`6.1.0` Introduced in WordPress core. @@ -239,8 +241,8 @@ A guide to the terms and variable names referenced by the Style Engine package.
Identifiers that describe stylistic, modifiable features of an HTML element. E.g., border, font-size, width...
CSS rule
A CSS selector followed by a CSS declarations block inside a set of curly braces. Usually found in a CSS stylesheet.
-
CSS selector
-
The first component of a CSS rule, a CSS selector is a pattern of elements, classnames or other terms that define the element to which the rule’s CSS definitions apply. E.g., p.my-cool-classname > span. See MDN CSS selectors article.
+
CSS selector (or CSS class selector)
+
The first component of a CSS rule, a CSS selector is a pattern of elements, classnames or other terms that define the element to which the rule’s CSS definitions apply. E.g., p.my-cool-classname > span. A CSS selector matches HTML elements based on the contents of the "class" attribute. See MDN CSS selectors article.
CSS stylesheet
A collection of CSS rules contained within a file or within an HTML style tag.
CSS value
diff --git a/packages/style-engine/class-wp-style-engine-css-declarations.php b/packages/style-engine/class-wp-style-engine-css-declarations.php index 17448a3486c79..243bc2b27d6cd 100644 --- a/packages/style-engine/class-wp-style-engine-css-declarations.php +++ b/packages/style-engine/class-wp-style-engine-css-declarations.php @@ -31,14 +31,14 @@ class WP_Style_Engine_CSS_Declarations { * If a `$declarations` array is passed, it will be used to populate * the initial $declarations prop of the object by calling add_declarations(). * - * @param array $declarations An array of declarations (property => value pairs). + * @param string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). */ public function __construct( $declarations = array() ) { $this->add_declarations( $declarations ); } /** - * Add a single declaration. + * Adds a single declaration. * * @param string $property The CSS property. * @param string $value The CSS value. @@ -46,28 +46,27 @@ public function __construct( $declarations = array() ) { * @return WP_Style_Engine_CSS_Declarations Returns the object to allow chaining methods. */ public function add_declaration( $property, $value ) { - - // Sanitize the property. + // Sanitizes the property. $property = $this->sanitize_property( $property ); - // Bail early if the property is empty. + // Bails early if the property is empty. if ( empty( $property ) ) { return $this; } - // Trim the value. If empty, bail early. + // Trims the value. If empty, bail early. $value = trim( $value ); if ( '' === $value ) { return $this; } - // Add the declaration property/value pair. + // Adds the declaration property/value pair. $this->declarations[ $property ] = $value; return $this; } /** - * Remove a single declaration. + * Removes a single declaration. * * @param string $property The CSS property. * @@ -79,7 +78,7 @@ public function remove_declaration( $property ) { } /** - * Add multiple declarations. + * Adds multiple declarations. * * @param array $declarations An array of declarations. * @@ -93,7 +92,7 @@ public function add_declarations( $declarations ) { } /** - * Remove multiple declarations. + * Removes multiple declarations. * * @param array $properties An array of properties. * @@ -107,7 +106,7 @@ public function remove_declarations( $properties = array() ) { } /** - * Get the declarations array. + * Gets the declarations array. * * @return array */ @@ -122,7 +121,7 @@ public function get_declarations() { * @param string $value The value to be filtered. * @param string $spacer The spacer between the colon and the value. Defaults to an empty string. * - * @return string The filtered declaration as a single string. + * @return string The filtered declaration or an empty string. */ protected static function filter_declaration( $property, $value, $spacer = '' ) { $filtered_value = wp_strip_all_tags( $value, true ); @@ -135,8 +134,8 @@ protected static function filter_declaration( $property, $value, $spacer = '' ) /** * Filters and compiles the CSS declarations. * - * @param boolean $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`. + * @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 The CSS declarations. */ @@ -158,7 +157,7 @@ public function get_declarations_string( $should_prettify = false, $indent_count } /** - * Sanitize property names. + * Sanitizes property names. * * @param string $property The CSS property. * 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 220bf1850e54a..f0f0c80ec5d6a 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -37,9 +37,9 @@ class WP_Style_Engine_CSS_Rule { /** * Constructor * - * @param string $selector The CSS selector. - * @param array|WP_Style_Engine_CSS_Declarations $declarations An array of declarations (property => value pairs), - * or a WP_Style_Engine_CSS_Declarations object. + * @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. */ public function __construct( $selector = '', $declarations = array() ) { $this->set_selector( $selector ); @@ -47,7 +47,7 @@ public function __construct( $selector = '', $declarations = array() ) { } /** - * Set the selector. + * Sets the selector. * * @param string $selector The CSS selector. * @@ -59,7 +59,7 @@ public function set_selector( $selector ) { } /** - * Set the declarations. + * Sets the declarations. * * @param array|WP_Style_Engine_CSS_Declarations $declarations An array of declarations (property => value pairs), * or a WP_Style_Engine_CSS_Declarations object. @@ -83,16 +83,16 @@ public function add_declarations( $declarations ) { } /** - * Get the declarations object. + * Gets the declarations object. * - * @return WP_Style_Engine_CSS_Declarations + * @return WP_Style_Engine_CSS_Declarations The declarations object. */ public function get_declarations() { return $this->declarations; } /** - * Get the full selector. + * Gets the full selector. * * @return string */ @@ -101,18 +101,18 @@ public function get_selector() { } /** - * Get the CSS. + * Gets the CSS. * - * @param boolean $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`. + * @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 ) { $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; $declarations_indent = $should_prettify ? $indent_count + 1 : 0; - $new_line = $should_prettify ? "\n" : ''; - $space = $should_prettify ? ' ' : ''; + $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 ); @@ -120,6 +120,6 @@ public function get_css( $should_prettify = false, $indent_count = 0 ) { return ''; } - return "{$rule_indent}{$selector}{$space}{{$new_line}{$css_declarations}{$new_line}{$rule_indent}}"; + return "{$rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$rule_indent}}"; } } diff --git a/packages/style-engine/class-wp-style-engine-css-rules-store.php b/packages/style-engine/class-wp-style-engine-css-rules-store.php index 936ef773ee78b..ae657ab69de43 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 @@ -42,7 +42,7 @@ class WP_Style_Engine_CSS_Rules_Store { protected $rules = array(); /** - * Get an instance of the store. + * Gets an instance of the store. * * @param string $store_name The name of the store. * @@ -61,7 +61,7 @@ public static function get_store( $store_name = 'default' ) { } /** - * Get an array of all available stores. + * Gets an array of all available stores. * * @return WP_Style_Engine_CSS_Rules_Store[] */ @@ -79,7 +79,7 @@ public static function remove_all_stores() { } /** - * Set the store name. + * Sets the store name. * * @param string $name The store name. * @@ -90,7 +90,7 @@ public function set_name( $name ) { } /** - * Get the store name. + * Gets the store name. * * @return string */ @@ -99,7 +99,7 @@ public function get_name() { } /** - * Get an array of all rules. + * Gets an array of all rules. * * @return WP_Style_Engine_CSS_Rule[] */ @@ -108,7 +108,7 @@ public function get_all_rules() { } /** - * Get a WP_Style_Engine_CSS_Rule object by its selector. + * 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. @@ -132,7 +132,7 @@ public function add_rule( $selector ) { } /** - * Remove a selector from the store. + * Removes a selector from the store. * * @param string $selector The CSS selector. * diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index d2d8e80ac6093..6b82f31944c6c 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -19,7 +19,7 @@ class WP_Style_Engine_Processor { /** - * The Style-Engine Store objects + * A collection of Style Engine Store objects. * * @var WP_Style_Engine_CSS_Rules_Store[] */ @@ -39,7 +39,16 @@ class WP_Style_Engine_Processor { * * @return WP_Style_Engine_Processor Returns the object to allow chaining methods. */ - public function add_store( WP_Style_Engine_CSS_Rules_Store $store ) { + public function add_store( $store ) { + if ( ! $store instanceof WP_Style_Engine_CSS_Rules_Store ) { + _doing_it_wrong( + __METHOD__, + __( '$store must be an instance of WP_Style_Engine_CSS_Rules_Store', 'default' ), + '6.1.0' + ); + return $this; + } + $this->stores[ $store->get_name() ] = $store; return $this; @@ -56,6 +65,7 @@ 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 ] ) ) { @@ -71,10 +81,12 @@ public function add_rules( $css_rules ) { /** * Get the CSS rules as a string. * - * @param array $options array( - * 'optimize' => (boolean) Whether to optimize the CSS output, e.g., combine rules. - * 'prettify' => (boolean) Whether to add new lines to output. - * );. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type bool $optimize Whether to optimize the CSS output, e.g., combine rules. Default is `false`. + * @type bool $prettify Whether to add new lines and indents to output. Default is the test of whether the global constant `SCRIPT_DEBUG` is defined. + * } * * @return string The computed CSS. */ diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index ab2bed13feed5..1d6926179f652 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -12,12 +12,11 @@ } /** - * Singleton class representing the style engine. + * Class WP_Style_Engine. * - * Consolidates rendering block styles to reduce duplication and streamline - * CSS styles generation. + * The Style Engine aims to provide a consistent API for rendering styling for blocks across both client-side and server-side applications. * - * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes). + * This class is for internal Core usage and is not supposed to be used by extenders (plugins and/or themes). * This is a low-level API that may need to do breaking changes. Please, use wp_style_engine_get_styles instead. * * @access private @@ -35,6 +34,8 @@ class WP_Style_Engine { * - property_keys => (array) array of keys whose values represent a valid CSS property, e.g., "margin" or "border". * - path => (array) a path that accesses the corresponding style value in the block style object. * - value_func => (string) the name of a function to generate a CSS definition array for a particular style object. The output of this function should be `array( "$property" => "$value", ... )`. + * + * @var array */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( 'color' => array( @@ -217,13 +218,13 @@ class WP_Style_Engine { /** * 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. + * @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. * * @return string The slug, or empty string if not found. */ protected static function get_slug_from_preset_value( $style_value, $property_key ) { - if ( is_string( $style_value ) && str_contains( $style_value, "var:preset|{$property_key}|" ) ) { + if ( is_string( $style_value ) && is_string( $property_key ) && str_contains( $style_value, "var:preset|{$property_key}|" ) ) { $index_to_splice = strrpos( $style_value, '|' ) + 1; return _wp_to_kebab_case( substr( $style_value, $index_to_splice ) ); } @@ -231,10 +232,10 @@ protected static function get_slug_from_preset_value( $style_value, $property_ke } /** - * Util: 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, e.g., var(--wp--preset--color--background) from a preset string such as `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. + * @param string $style_value A single CSS preset value. + * @param string[] $css_vars An associate array of CSS var patterns used to generate the var string. * * @return string The css var, or an empty string if no match for slug found. */ @@ -257,7 +258,7 @@ protected static function get_css_var_value( $style_value, $css_vars ) { * * @param string? $style_value A single css preset value. * - * @return boolean + * @return bool */ protected static function is_valid_style_value( $style_value ) { return '0' === $style_value || ! empty( $style_value ); @@ -266,9 +267,9 @@ protected static function is_valid_style_value( $style_value ) { /** * Stores a CSS rule using the provided CSS selector and CSS declarations. * - * @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 array $css_declarations An array of parsed CSS property => CSS value pairs. + * @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" ). * * @return void. */ @@ -294,16 +295,21 @@ public static function get_store( $store_name ) { * 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. * + * @since 6.1.0 + * * @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`. - * );. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset||`, to var( --wp--preset--* ) values. Default `false`. + * @type string $selector Optional. When a selector is passed, the value of `$css` in the return value will comprise a full CSS rule `$selector { ...$css_declarations }`, + * otherwise, the value will be a concatenated string of CSS declarations. + * } * - * @return array array( - * 'declarations' => (array) An array of parsed CSS property => CSS value pairs. - * 'classnames' => (array) A flat array of classnames. - * ); + * @return array { + * @type string $classnames Classnames separated by a space. + * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * } */ public static function parse_block_styles( $block_styles, $options ) { $parsed_styles = array( @@ -335,12 +341,12 @@ public static function parse_block_styles( $block_styles, $options ) { } /** - * Returns classnames, and generates classname(s) from a CSS preset property pattern, e.g., 'var:preset|color|heavenly-blue'. + * Returns classnames, and generates classname(s) from a CSS preset property pattern, e.g., '`var:preset||`'. * - * @param array $style_value A single raw style value or css preset property from the generate() $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_value A single raw style value or css preset property from the $block_styles array. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. * - * @return array An array of CSS classnames. + * @return array|string[] An array of CSS classnames, or empty array. */ protected static function get_classnames( $style_value, $style_definition ) { if ( empty( $style_value ) ) { @@ -357,10 +363,12 @@ protected static function get_classnames( $style_value, $style_definition ) { $slug = static::get_slug_from_preset_value( $style_value, $property_key ); if ( $slug ) { - // Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA. - // One day, if there are no stored schemata, we could allow custom patterns or - // generate classnames based on other properties - // such as a path or a value or a prefix passed in options. + /* + * Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA. + * One day, if there are no stored schemata, we could allow custom patterns or + * generate classnames based on other properties + * such as a path or a value or a prefix passed in options. + */ $classnames[] = strtr( $classname, array( '$slug' => $slug ) ); } } @@ -372,15 +380,19 @@ protected static function get_classnames( $style_value, $style_definition ) { /** * Returns an array of CSS declarations based on valid block style values. * - * @param array $style_value A single raw style value from the generate() $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. - * @param array $options array( - * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. - * );. + * @since 6.1.0 * - * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). + * @param array $style_value A single raw style value from $block_styles array. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset||`, to var( --wp--preset--* ) values. Default `false`. + * } + * + * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). */ - protected static function get_css_declarations( $style_value, $style_definition, $options ) { + protected static function get_css_declarations( $style_value, $style_definition, $options = array() ) { if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options ); } @@ -389,8 +401,10 @@ protected static function get_css_declarations( $style_value, $style_definition, $style_property_keys = $style_definition['property_keys']; $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; - // Build CSS var values from var:? values, e.g, `var(--wp--css--rule-slug )` - // Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition. + /* + * Build CSS var values from `var:preset||` values, e.g, `var(--wp--css--rule-slug )`. + * Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition. + */ if ( is_string( $style_value ) && str_contains( $style_value, 'var:' ) ) { if ( ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) { $css_var = static::get_css_var_value( $style_value, $style_definition['css_vars'] ); @@ -401,9 +415,11 @@ protected static function get_css_declarations( $style_value, $style_definition, return $css_declarations; } - // Default rule builder. - // If the input contains an array, assume box model-like properties - // for styles such as margins and padding. + /* + * Default rule builder. + * If the input contains an array, assume box model-like properties + * for styles such as margins and padding. + */ if ( is_array( $style_value ) ) { // Bail out early if the `'individual'` property is not defined. if ( ! isset( $style_property_keys['individual'] ) ) { @@ -436,22 +452,26 @@ protected static function get_css_declarations( $style_value, $style_definition, * "border-{top|right|bottom|left}-{color|width|style}: {value};" or, * "border-image-{outset|source|width|repeat|slice}: {value};" * - * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. - * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. - * @param array $options array( - * 'convert_vars_to_classnames' => (boolean) Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. - * );. + * @param array $style_value A single raw style value from $block_styles array. + * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA representing an individual property of a CSS property, e.g., 'top' in 'border-top'. + * @param array $options { + * Optional. An array of options. Default empty array. * - * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). + * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset||`, to var( --wp--preset--* ) values. Default `false`. + * } + * + * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). */ - protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options ) { + protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options = array() ) { if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) { return array(); } - // The first item in $individual_property_definition['path'] array tells us the style property, e.g., "border". - // We use this to get a corresponding CSS style definition such as "color" or "width" from the same group. - // The second item in $individual_property_definition['path'] array refers to the individual property marker, e.g., "top". + /* + * The first item in $individual_property_definition['path'] array tells us the style property, e.g., "border". + * We use this to get a corresponding CSS style definition such as "color" or "width" from the same group. + * The second item in $individual_property_definition['path'] array refers to the individual property marker, e.g., "top". + */ $definition_group_key = $individual_property_definition['path'][0]; $individual_property_key = $individual_property_definition['path'][1]; $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; @@ -481,8 +501,8 @@ protected static function get_individual_property_css_declarations( $style_value /** * 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. + * @param string[] $css_declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * @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. */ @@ -505,10 +525,12 @@ public static function compile_css( $css_declarations, $css_selector ) { * 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. - * @param array $options array( - * 'optimize' => (boolean) Whether to optimize the CSS output, e.g., combine rules. - * 'prettify' => (boolean) Whether to add new lines to output. - * );. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type bool $optimize Whether to optimize the CSS output, e.g., combine rules. Default is `false`. + * @type bool $prettify Whether to add new lines and indents to output. Default is the test of whether the global constant `SCRIPT_DEBUG` is defined. + * } * * @return string A compiled stylesheet from stored CSS rules. */ @@ -518,137 +540,3 @@ public static function compile_stylesheet_from_css_rules( $css_rules, $options = return $processor->get_css( $options ); } } - -/** - * 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/ - * - * Example usage: - * - * $styles = wp_style_engine_get_styles( array( 'color' => array( 'text' => '#cccccc' ) ) ); - * // Returns `array( 'css' => 'color: #cccccc', 'declarations' => array( 'color' => '#cccccc' ), 'classnames' => 'has-color' )`. - * - * @access public - * - * @param array $block_styles The style object. - * @param array $options array( - * 'context' => (string|null) An identifier describing the origin of the style object, e.g., 'block-supports' or 'global-styles'. Default is 'block-supports'. - * When set, the style engine will attempt to store the CSS rules, where a selector is also passed. - * '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 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. - * ); - */ -function wp_style_engine_get_styles( $block_styles, $options = array() ) { - if ( ! class_exists( 'WP_Style_Engine' ) ) { - return array(); - } - - $options = wp_parse_args( - $options, - array( - 'selector' => null, - 'context' => null, - 'convert_vars_to_classnames' => false, - ) - ); - - $parsed_styles = WP_Style_Engine::parse_block_styles( $block_styles, $options ); - - // Output. - $styles_output = array(); - - if ( ! empty( $parsed_styles['declarations'] ) ) { - $styles_output['css'] = WP_Style_Engine::compile_css( $parsed_styles['declarations'], $options['selector'] ); - $styles_output['declarations'] = $parsed_styles['declarations']; - if ( ! empty( $options['context'] ) ) { - WP_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 ); -} - -/** - * 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( - * array( - * 'selector' => (string) A CSS selector. - * declarations' => (boolean) An array of CSS definitions, e.g., array( "$property" => "$value" ). - * ) - * );. - * @param array $options array( - * 'context' => (string|null) An identifier describing the origin of the style object, e.g., 'block-supports' or 'global-styles'. Default is 'block-supports'. - * When set, the style engine will attempt to store the CSS rules. - * 'optimize' => (boolean) Whether to optimize the CSS output, e.g., combine rules. - * 'prettify' => (boolean) Whether to add new lines to output. - * );. - * - * @return string A compiled CSS string. - */ -function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = array() ) { - if ( ! class_exists( 'WP_Style_Engine' ) || empty( $css_rules ) ) { - return ''; - } - - $options = wp_parse_args( - $options, - array( - 'context' => null, - ) - ); - - $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; - } - - if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'] ); - } - - $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, $options ); -} - -/** - * Returns compiled CSS from a store, if found. - * - * @access public - * - * @param string $store_name A valid store name. - * @param array $options array( - * 'optimize' => (boolean) Whether to optimize the CSS output, e.g., combine rules. - * 'prettify' => (boolean) Whether to add new lines to output. - * );. - * @return string A compiled CSS string. - */ -function wp_style_engine_get_stylesheet_from_context( $store_name, $options = array() ) { - if ( ! class_exists( 'WP_Style_Engine' ) || empty( $store_name ) ) { - return ''; - } - - return WP_Style_Engine::compile_stylesheet_from_css_rules( WP_Style_Engine::get_store( $store_name )->get_all_rules(), $options ); -} diff --git a/packages/style-engine/docs/using-the-style-engine-with-block-supports.md b/packages/style-engine/docs/using-the-style-engine-with-block-supports.md new file mode 100644 index 0000000000000..5e7c82574f703 --- /dev/null +++ b/packages/style-engine/docs/using-the-style-engine-with-block-supports.md @@ -0,0 +1,151 @@ +# Using the Style Engine to generate block supports styles + +[Block supports](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) is the API that allows a block to declare support for certain features. + +Where a block declares support for a specific style group or property, e.g., "spacing" or "spacing.padding", the block's attributes are extended to include a **style object**. + +For example: + +```json +{ + "attributes": { + "style": { + "spacing": { + "margin": { + "top": "10px" + }, + "padding": "1em" + }, + "typography": { + "fontSize": "2.2rem" + } + } + } +} +``` + +Using this object, the Style Engine can generate the classes and CSS required to style the block element. + +The global function `wp_style_engine_get_styles` accepts a style object as its first argument, and will output compiled CSS and an array of CSS declaration property/value pairs. + +```php +$block_styles = array( + 'spacing' => array( 'padding' => '10px', 'margin' => array( 'top' => '1em') ), + 'typography' => array( 'fontSize' => '2.2rem' ), +); +$styles = wp_style_engine_get_styles( + $block_styles +); +print_r( $styles ); + +/* +array( + 'css' => 'padding:10px;margin-top:1em;font-size:2.2rem', + 'declarations' => array( 'padding' => '10px', 'margin-top' => '1em', 'font-size' => '2.2rem' ) +) +*/ +``` + +## Checking for block support and skip serialization + +Before passing the block style object to the Style Engine, it's important to take into account: + +1. whether the theme has elected to support a particular block style, and +2. whether a block has elected to "skip serialization" of that particular block style, that is, opt-out of automatic application of styles to the block's element (usually in order to do it via the block's internals). See the [block API documentation](https://developer.wordpress.org/block-editor/explanations/architecture/styles/#block-supports-api) for further information. + +If a block either: + +- has no support for a specific style, or +- skips serialization of that style + +it's likely that you'll want to remove those style values from the style object. + +For example: + +```php +// Check if a block has support using block_has_support (https://developer.wordpress.org/reference/functions/block_has_support/) +$has_padding_support = block_has_support( $block_type, array( 'spacing', 'padding' ), false ); // Returns true. +$has_margin_support = block_has_support( $block_type, array( 'spacing', 'margin' ), false ); // Returns false. + +// Check skipping of serialization. +$should_skip_padding = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'padding' ); // Returns true. +$should_skip_margin = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'margin' ); // Returns false. + +// Now build the styles object. +$spacing_block_styles = array(); +$spacing_block_styles['padding'] = $has_padding_support && ! $skip_padding ? _wp_array_get( $block_attributes['style'], array( 'spacing', 'padding' ), null ) : null; +$spacing_block_styles['margin'] = $has_margin_support && ! $skip_margin ? _wp_array_get( $block_attributes['style'], array( 'spacing', 'margin' ), null ) : null; + +// Now get the styles. +$styles = wp_style_engine_get_styles( array( 'spacing' => $spacing_block_styles ) ); + +print_r( $styles ); + +/* +// Nothing, because there's no support for margin and the block skip's serialization for padding. +array() +*/ +``` + +## Generating classnames and CSS custom selectors from presets + +Many of theme.json's presets will generate both CSS custom properties and CSS rules (consisting of a selector and the CSS declarations) on the frontend. + +Styling a block using these presets normally involves adding the selector to the "className" attribute of the block. + +For styles that can have preset values, such as text color and font family, the Style Engine knows how to construct the classnames using the preset slug. + +To discern CSS values from preset values however, the Style Engine expects a special format. + +Preset values must follow the pattern `var:preset||`. + +When the Style Engine encounters these values, it will parse them and create a CSS value of `var(--wp--preset--font-size--small)` and/or generate a classname if required. + +Example: + +```php +// Let's say the block attributes styles contain a fontSize preset slug of "small". +$preset_font_size = "var:preset|font-size|{$block_attributes['fontSize']}"; +// Now let's say the block attributes styles contain a backgroundColor preset slug of "blue". +$preset_background_color = "var:preset|color|{$block_attributes['backgroundColor']}"; + +$block_styles = array( + 'typography' => array( 'fontSize' => $preset_font_size ), + 'color' => array( 'background' => $preset_background_color ) +); + +$styles = wp_style_engine_get_styles( + $block_styles +); +print_r( $styles ); + +/* +array( + 'css' => 'background-color:var(--wp--preset--color--blue);font-size:var(--wp--preset--font-size--small);', + 'classnames' => 'has-background-color has-blue-background-color has-small-font-size', +) +*/ +``` + +If you don't want the Style Engine to output the CSS custom vars as well, which you might not if you're applying both the CSS and classnames to the block element, you can pass `'convert_vars_to_classnames' => true` in the options array. + +```php +$options = array( + // Whether to skip converting CSS var:? values to var( --wp--preset--* ) values. Default is `false`. + 'convert_vars_to_classnames' => 'true', +); +$styles = wp_style_engine_get_styles( + $block_styles, + $options +); +print_r( $styles ); + +/* +array( + 'css' => 'letter-spacing:12px;', // non-preset-based CSS will still be compiled. + 'classnames' => 'has-background-color has-blue-background-color has-small-font-size', +) +*/ +``` + +Read more about [global styles](https://developer.wordpress.org/block-editor/explanations/architecture/styles/#global-styles) and [preset CSS custom properties](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/#css-custom-properties-presets-custom) and [theme supports](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/). diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json index e19654cae6a92..faedd75f5e201 100644 --- a/packages/style-engine/package.json +++ b/packages/style-engine/package.json @@ -1,13 +1,14 @@ { "name": "@wordpress/style-engine", - "version": "0.16.0", - "description": "WordPress Style engine.", + "version": "1.0.0-prerelease", + "description": "A suite of parsers and compilers for WordPress styles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "gutenberg", "styles", + "css", "global styles" ], "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/style-engine/README.md", diff --git a/packages/style-engine/phpunit/class-wp-style-engine-css-declarations-test.php b/packages/style-engine/phpunit/class-wp-style-engine-css-declarations-test.php index e36178ca4d8e5..bbd272b1f9beb 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-css-declarations-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-css-declarations-test.php @@ -6,16 +6,26 @@ * @subpackage style-engine */ -require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +// Check for the existence of Style Engine classes and methods. +// Once the Style Engine has been migrated to Core we can remove the if statements and require imports. +// Testing new features from the Gutenberg package may require +// testing against `gutenberg_` and `_Gutenberg` functions and methods in the future. +if ( ! class_exists( 'WP_Style_Engine_CSS_Declarations' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +} /** - * Tests for registering, storing and generating CSS declarations. + * Tests registering, storing and generating CSS declarations. + * + * @coversDefaultClass WP_Style_Engine_CSS_Declarations */ class WP_Style_Engine_CSS_Declarations_Test extends WP_UnitTestCase { /** - * Should set declarations on instantiation. + * Tests setting declarations on instantiation. + * + * @covers ::__construct */ - public function test_instantiate_with_declarations() { + public function test_should_instantiate_with_declarations() { $input_declarations = array( 'margin-top' => '10px', 'font-size' => '2rem', @@ -25,22 +35,29 @@ public function test_instantiate_with_declarations() { } /** - * Should add declarations. + * Tests that declarations are added. + * + * @covers ::add_declarations + * @covers ::add_declaration */ - public function test_add_declarations() { + public function test_should_add_declarations() { $input_declarations = array( 'padding' => '20px', 'color' => 'var(--wp--preset--elbow-patches)', ); $css_declarations = new WP_Style_Engine_CSS_Declarations(); $css_declarations->add_declarations( $input_declarations ); + $this->assertSame( $input_declarations, $css_declarations->get_declarations() ); } /** - * Should add declarations. + * Tests that new declarations are added to existing declarations. + * + * @covers ::add_declarations + * @covers ::add_declaration */ - public function test_add_a_single_declaration() { + public function test_should_add_new_declarations_to_existing() { $input_declarations = array( 'border-width' => '1%', 'background-color' => 'var(--wp--preset--english-mustard)', @@ -50,13 +67,17 @@ public function test_add_a_single_declaration() { 'letter-spacing' => '1.5px', ); $css_declarations->add_declarations( $extra_declaration ); + $this->assertSame( array_merge( $input_declarations, $extra_declaration ), $css_declarations->get_declarations() ); } /** - * Should sanitize properties before storing. + * Tests that properties are sanitized before storing. + * + * @covers ::filter_declaration + * @covers ::sanitize_property */ - public function test_sanitize_properties() { + public function test_should_sanitize_properties() { $input_declarations = array( '^--wp--style--sleepy-potato$' => '40px', '' => 'var(--wp--preset--english-mustard)', @@ -73,123 +94,141 @@ public function test_sanitize_properties() { } /** - * Should compile css declarations into a css declarations block string. + * Tests that values with HTML tags are escaped, and CSS properties are run through safecss_filter_attr(). + * + * @covers ::get_declarations_string + * @covers ::filter_declaration */ - public function test_generate_css_declarations_string() { - $input_declarations = array( - 'color' => 'red', - 'border-top-left-radius' => '99px', - 'text-decoration' => 'underline', + public function test_should_strip_html_tags_and_remove_unsafe_css_properties() { + $input_declarations = array( + 'font-size' => '', + 'padding' => '', + 'potato' => 'uppercase', + 'cheese' => '10px', + 'margin-right' => '10em', ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $safe_style_css_mock_action = new MockAction(); - $this->assertSame( - 'color:red;border-top-left-radius:99px;text-decoration:underline;', - $css_declarations->get_declarations_string() - ); - } + // filter_declaration() is called in get_declarations_string(). + add_filter( 'safe_style_css', array( $safe_style_css_mock_action, 'filter' ) ); + $css_declarations_string = $css_declarations->get_declarations_string(); - /** - * Should compile css declarations into a prettified css declarations block string. - */ - public function test_generate_prettified_css_declarations_string() { - $input_declarations = array( - 'color' => 'red', - 'border-top-left-radius' => '99px', - 'text-decoration' => 'underline', + $this->assertSame( + 3, // Values with HTML tags are removed first by wp_strip_all_tags(). + $safe_style_css_mock_action->get_call_count(), + '"safe_style_css" filters were not applied to CSS declaration properties.' ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); $this->assertSame( - 'color: red; border-top-left-radius: 99px; text-decoration: underline;', - $css_declarations->get_declarations_string( true ) + 'margin-right:10em;', + $css_declarations_string, + 'Unallowed CSS properties or values with HTML tags were not removed.' ); } + /** - * Should compile css declarations into a prettified and indented css declarations block string. + * Tests that calc, clamp, min, max, and minmax CSS functions are allowed. + * + * @covers ::get_declarations_string + * @covers ::filter_declaration */ - public function test_generate_prettified_with_indent_css_declarations_string() { - $input_declarations = array( - 'color' => 'red', - 'border-top-left-radius' => '99px', - 'text-decoration' => 'underline', + public function test_should_allow_css_functions_and_strip_unsafe_css_values() { + $input_declarations = array( + 'background' => 'var(--wp--preset--color--primary, 10px)', // Simple var(). + 'font-size' => 'clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem)', // Nested clamp(). + 'width' => 'min(150vw, 100px)', + 'min-width' => 'max(150vw, 100px)', + 'max-width' => 'minmax(400px, 50%)', + 'padding' => 'calc(80px * -1)', + 'background-image' => 'url("https://wordpress.org")', + 'line-height' => 'url("https://wordpress.org")', + 'margin' => 'illegalfunction(30px)', ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $safecss_filter_attr_allow_css_mock_action = new MockAction(); - $this->assertSame( - ' color: red; - border-top-left-radius: 99px; - text-decoration: underline;', - $css_declarations->get_declarations_string( true, 1 ) - ); - } + // filter_declaration() is called in get_declarations_string(). + add_filter( 'safecss_filter_attr_allow_css', array( $safecss_filter_attr_allow_css_mock_action, 'filter' ) ); + $css_declarations_string = $css_declarations->get_declarations_string(); - /** - * Should compile css declarations into a css declarations block string. - */ - public function test_generate_prettified_with_more_indents_css_declarations_string() { - $input_declarations = array( - 'color' => 'red', - 'border-top-left-radius' => '99px', - 'text-decoration' => 'underline', + $this->assertSame( + 9, + $safecss_filter_attr_allow_css_mock_action->get_call_count(), + '"safecss_filter_attr_allow_css" filters were not applied to CSS declaration values.' ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); $this->assertSame( - ' color: red; - border-top-left-radius: 99px; - text-decoration: underline;', - $css_declarations->get_declarations_string( true, 2 ) + 'background:var(--wp--preset--color--primary, 10px);font-size:clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem);width:min(150vw, 100px);min-width:max(150vw, 100px);max-width:minmax(400px, 50%);padding:calc(80px * -1);background-image:url("https://wordpress.org");', + $css_declarations_string, + 'Unsafe values were not removed' ); } /** - * Should escape values and run the CSS through safecss_filter_attr. + * Tests that CSS declarations are compiled into a CSS declarations block string. + * + * @covers ::get_declarations_string + * + * @dataProvider data_should_compile_css_declarations_to_css_declarations_string + * + * @param string $expected The expected declarations block string. + * @param bool $should_prettify Optional. Whether to pretty the string. Default false. + * @param int $indent_count Optional. The number of tab indents. Default false. */ - public function test_remove_unsafe_properties_and_values() { + public function test_should_compile_css_declarations_to_css_declarations_string( $expected, $should_prettify = false, $indent_count = 0 ) { $input_declarations = array( - 'color' => 'url("https://wordpress.org")', - 'font-size' => '', - 'margin-right' => '10em', - 'padding' => '', - 'potato' => 'uppercase', + 'color' => 'red', + 'border-top-left-radius' => '99px', + 'text-decoration' => 'underline', ); $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); $this->assertSame( - 'margin-right:10em;', - $css_declarations->get_declarations_string() + $expected, + $css_declarations->get_declarations_string( $should_prettify, $indent_count ) ); } /** - * Should allow calc, clamp, min, max, and minmax CSS functions. + * Data provider for test_should_compile_css_declarations_to_css_declarations_string(). + * + * @return array */ - public function test_allow_particular_css_functions() { - $input_declarations = array( - 'background' => 'var(--wp--preset--color--primary, 10px)', // Simple var(). - 'font-size' => 'clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem)', // Nested clamp(). - 'width' => 'min(150vw, 100px)', - 'min-width' => 'max(150vw, 100px)', - 'max-width' => 'minmax(400px, 50%)', - 'padding' => 'calc(80px * -1)', - 'background-image' => 'url("https://wordpress.org")', - 'line-height' => 'url("https://wordpress.org")', - 'margin' => 'illegalfunction(30px)', - ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); - - $this->assertSame( - 'background:var(--wp--preset--color--primary, 10px);font-size:clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem);width:min(150vw, 100px);min-width:max(150vw, 100px);max-width:minmax(400px, 50%);padding:calc(80px * -1);background-image:url("https://wordpress.org");', - $css_declarations->get_declarations_string() + public function data_should_compile_css_declarations_to_css_declarations_string() { + return array( + 'unprettified, no indent' => array( + 'expected' => 'color:red;border-top-left-radius:99px;text-decoration:underline;', + ), + 'unprettified, one indent' => array( + 'expected' => 'color:red;border-top-left-radius:99px;text-decoration:underline;', + 'should_prettify' => false, + 'indent_count' => 1, + ), + 'prettified, no indent' => array( + 'expected' => 'color: red; border-top-left-radius: 99px; text-decoration: underline;', + 'should_prettify' => true, + ), + 'prettified, one indent' => array( + 'expected' => "\tcolor: red;\n\tborder-top-left-radius: 99px;\n\ttext-decoration: underline;", + 'should_prettify' => true, + 'indent_count' => 1, + ), + 'prettified, two indents' => array( + 'expected' => "\t\tcolor: red;\n\t\tborder-top-left-radius: 99px;\n\t\ttext-decoration: underline;", + 'should_prettify' => true, + 'indent_count' => 2, + ), ); } /** - * Should remove a declaration + * Tests removing a single declaration. + * + * @covers ::remove_declaration */ - public function test_remove_declaration() { + public function test_should_remove_single_declaration() { $input_declarations = array( 'color' => 'tomato', 'margin' => '10em 10em 20em 1px', @@ -199,20 +238,25 @@ public function test_remove_declaration() { $this->assertSame( 'color:tomato;margin:10em 10em 20em 1px;font-family:Happy Font serif;', - $css_declarations->get_declarations_string() + $css_declarations->get_declarations_string(), + 'CSS declarations string does not match the values of `$declarations` passed to the constructor.' ); $css_declarations->remove_declaration( 'color' ); + $this->assertSame( 'margin:10em 10em 20em 1px;font-family:Happy Font serif;', - $css_declarations->get_declarations_string() + $css_declarations->get_declarations_string(), + 'Output after removing "color" declaration via `remove_declaration()` does not match expectations' ); } /** - * Should remove declarations + * Tests that multiple declarations are removed. + * + * @covers ::remove_declarations */ - public function test_remove_declarations() { + public function test_should_remove_multiple_declarations() { $input_declarations = array( 'color' => 'cucumber', 'margin' => '10em 10em 20em 1px', @@ -222,13 +266,16 @@ public function test_remove_declarations() { $this->assertSame( 'color:cucumber;margin:10em 10em 20em 1px;font-family:Happy Font serif;', - $css_declarations->get_declarations_string() + $css_declarations->get_declarations_string(), + 'CSS declarations string does not match the values of `$declarations` passed to the constructor.' ); $css_declarations->remove_declarations( array( 'color', 'margin' ) ); + $this->assertSame( 'font-family:Happy Font serif;', - $css_declarations->get_declarations_string() + $css_declarations->get_declarations_string(), + 'Output after removing "color" and "margin" declarations via `remove_declarations()` does not match expectations' ); } } diff --git a/packages/style-engine/phpunit/class-wp-style-engine-css-rule-test.php b/packages/style-engine/phpunit/class-wp-style-engine-css-rule-test.php index 06ccc003e7cda..08aa84ce32d11 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-css-rule-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-css-rule-test.php @@ -6,17 +6,30 @@ * @subpackage style-engine */ -require __DIR__ . '/../class-wp-style-engine-css-rule.php'; -require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +// Check for the existence of Style Engine classes and methods. +// Once the Style Engine has been migrated to Core we can remove the if statements and require imports. +// Testing new features from the Gutenberg package may require +// testing against `gutenberg_` and `_Gutenberg` functions and methods in the future. +if ( ! class_exists( 'WP_Style_Engine_CSS_Declarations' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +} + +if ( ! class_exists( 'WP_Style_Engine_CSS_Rule' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-rule.php'; +} /** - * Tests for registering, storing and generating CSS declarations. + * Tests for registering, storing and generating CSS rules. + * + * @coversDefaultClass WP_Style_Engine_CSS_Rule */ class WP_Style_Engine_CSS_Rule_Test extends WP_UnitTestCase { /** - * Should set declarations on instantiation. + * Tests that declarations are set on instantiation. + * + * @covers ::__construct */ - public function test_instantiate_with_selector_and_rules() { + public function test_should_instantiate_with_selector_and_rules() { $selector = '.law-and-order'; $input_declarations = array( 'margin-top' => '10px', @@ -25,16 +38,20 @@ public function test_instantiate_with_selector_and_rules() { $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $css_declarations ); - $this->assertSame( $selector, $css_rule->get_selector() ); + $this->assertSame( $selector, $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); $expected = "$selector{{$css_declarations->get_declarations_string()}}"; - $this->assertSame( $expected, $css_rule->get_css() ); + + $this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' ); } /** - * Test dedupe declaration properties. + * Tests that declaration properties are deduplicated. + * + * @covers ::add_declarations + * @covers ::get_css */ - public function test_dedupe_properties_in_rules() { + public function test_should_dedupe_properties_in_rules() { $selector = '.taggart'; $first_declaration = array( 'font-size' => '2rem', @@ -50,9 +67,12 @@ public function test_dedupe_properties_in_rules() { } /** - * Should add declarations. + * Tests that declarations can be added to existing rules. + * + * @covers ::add_declarations + * @covers ::get_css */ - public function test_add_declarations() { + public function test_should_add_declarations_to_existing_rules() { // Declarations using a WP_Style_Engine_CSS_Declarations object. $some_css_declarations = new WP_Style_Engine_CSS_Declarations( array( 'margin-top' => '10px' ) ); // Declarations using a property => value array. @@ -61,27 +81,32 @@ public function test_add_declarations() { $css_rule->add_declarations( $some_more_css_declarations ); $expected = '.hill-street-blues{margin-top:10px;font-size:1rem;}'; + $this->assertSame( $expected, $css_rule->get_css() ); } /** - * Should set selector. + * Tests setting a selector to a rule. + * + * @covers ::set_selector */ - public function test_set_selector() { + public function test_should_set_selector() { $selector = '.taggart'; $css_rule = new WP_Style_Engine_CSS_Rule( $selector ); - $this->assertSame( $selector, $css_rule->get_selector() ); + $this->assertSame( $selector, $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); $css_rule->set_selector( '.law-and-order' ); - $this->assertSame( '.law-and-order', $css_rule->get_selector() ); + $this->assertSame( '.law-and-order', $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to set_selector().' ); } /** - * Should generate CSS rules. + * Tests generating a CSS rule string. + * + * @covers ::get_css */ - public function test_get_css() { + public function test_should_generate_css_rule_string() { $selector = '.chips'; $input_declarations = array( 'margin-top' => '10px', @@ -95,9 +120,11 @@ public function test_get_css() { } /** - * Should return empty string with no declarations. + * Tests that an empty string will be returned where there are no declarations in a CSS rule. + * + * @covers ::get_css */ - public function test_get_css_no_declarations() { + public function test_should_return_empty_string_with_no_declarations() { $selector = '.holmes'; $input_declarations = array(); $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); @@ -107,9 +134,11 @@ public function test_get_css_no_declarations() { } /** - * Should generate prettified CSS rules. + * Tests that CSS rules are prettified. + * + * @covers ::get_css */ - public function test_get_prettified_css() { + public function test_should_prettify_css_rule_output() { $selector = '.baptiste'; $input_declarations = array( 'margin-left' => '0', 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 0f85a2e88c748..e781f5aa49334 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 @@ -6,63 +6,93 @@ * @subpackage style-engine */ -require __DIR__ . '/../class-wp-style-engine-css-rules-store.php'; -require __DIR__ . '/../class-wp-style-engine-css-rule.php'; -require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +// Check for the existence of Style Engine classes and methods. +// Once the Style Engine has been migrated to Core we can remove the if statements and require imports. +// Testing new features from the Gutenberg package may require +// testing against `gutenberg_` and `_Gutenberg` functions and methods in the future. +if ( ! class_exists( 'WP_Style_Engine_CSS_Declarations' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-declarations.php'; +} + +if ( ! class_exists( 'WP_Style_Engine_CSS_Rule' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-rule.php'; +} + +if ( ! class_exists( 'WP_Style_Engine_CSS_Rules_Store' ) ) { + require __DIR__ . '/../class-wp-style-engine-css-rules-store.php'; +} /** - * Tests for registering, storing and retrieving CSS Rules. + * Tests for registering, storing and retrieving a collection of CSS Rules (a store). + * + * @coversDefaultClass WP_Style_Engine_CSS_Rules_Store */ class WP_Style_Engine_CSS_Rules_Store_Test extends WP_UnitTestCase { /** - * Tear down after each test. + * Cleans up stores after each test. */ public function tear_down() { - parent::tear_down(); WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + parent::tear_down(); } + /** - * Should create a new store. + * Tests creating a new store on instantiation. + * + * @covers ::__construct */ - public function test_create_new_store() { + public function test_should_create_new_store_on_instantiation() { $new_pancakes_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'pancakes-with-strawberries' ); + $this->assertInstanceOf( 'WP_Style_Engine_CSS_Rules_Store', $new_pancakes_store ); } /** - * Should not create a new store with invalid $store_name. + * Tests that a `$store_name` argument is required and no store will be created without one. + * + * @covers ::get_store */ - public function test_store_name_required() { + public function test_should_not_create_store_without_a_store_name() { $not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( '' ); - $this->assertEmpty( $not_a_store ); + + $this->assertEmpty( $not_a_store, 'get_store() did not return an empty value with empty string as argument.' ); $also_not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( 123 ); - $this->assertEmpty( $also_not_a_store ); + + $this->assertEmpty( $also_not_a_store, 'get_store() did not return an empty value with number as argument.' ); $definitely_not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( null ); - $this->assertEmpty( $definitely_not_a_store ); + + $this->assertEmpty( $definitely_not_a_store, 'get_store() did not return an empty value with `null` as argument.' ); } /** - * Should return previously created store when the same selector key is passed. + * Tests returning a previously created store when the same selector key is passed. + * + * @covers ::get_store */ - public function test_get_store() { + public function test_should_return_existing_store() { $new_fish_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'fish-n-chips' ); $selector = '.haddock'; - $new_fish_store->add_rule( $selector )->get_selector(); - $this->assertEquals( $selector, $new_fish_store->add_rule( $selector )->get_selector() ); + $new_fish_store->add_rule( $selector ); + + $this->assertSame( $selector, $new_fish_store->add_rule( $selector )->get_selector(), 'Selector string of store rule does not match expected value' ); $the_same_fish_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'fish-n-chips' ); - $this->assertEquals( $selector, $the_same_fish_store->add_rule( $selector )->get_selector() ); + + $this->assertSame( $selector, $the_same_fish_store->add_rule( $selector )->get_selector(), 'Selector string of existing store rule does not match expected value' ); } /** - * Should return all previously created stores. + * Tests returning all previously created stores. + * + * @covers ::get_stores */ - public function test_get_stores() { + public function test_should_get_all_existing_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, @@ -73,34 +103,43 @@ public function test_get_stores() { } /** - * Should delete all previously created stores. + * Tests that all previously created stores are deleted. + * + * @covers ::remove_all_stores */ - public function test_remove_all_stores() { + public function test_should_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::get_stores(), + 'Return value of get_stores() does not match expectation' ); WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + $this->assertEquals( array(), - WP_Style_Engine_CSS_Rules_Store::get_stores() + WP_Style_Engine_CSS_Rules_Store::get_stores(), + 'Return value of get_stores() is not an empty array after remove_all_stores() called.' ); } /** - * Should return a stored rule. + * Tests adding rules to an existing store. + * + * @covers ::add_rule */ - public function test_add_rule() { + public function test_should_add_rule_to_existing_store() { $new_pie_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'meat-pie' ); $selector = '.wp-block-sauce a:hover'; $store_rule = $new_pie_store->add_rule( $selector ); $expected = ''; - $this->assertEquals( $expected, $store_rule->get_css() ); + + $this->assertSame( $expected, $store_rule->get_css(), 'Return value of get_css() is not a empty string where a rule has no CSS declarations.' ); $pie_declarations = array( 'color' => 'brown', @@ -112,13 +151,16 @@ public function test_add_rule() { $store_rule = $new_pie_store->add_rule( $selector ); $expected = "$selector{{$css_declarations->get_declarations_string()}}"; - $this->assertEquals( $expected, $store_rule->get_css() ); + + $this->assertSame( $expected, $store_rule->get_css(), 'Return value of get_css() does not match expected CSS from existing store rules.' ); } /** - * Should return all stored rules. + * Tests that all stored rule objects are returned. + * + * @covers ::get_all_rules */ - public function test_get_all_rules() { + public function test_should_get_all_rule_objects_for_a_store() { $new_pizza_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'pizza-with-mozzarella' ); $selector = '.wp-block-anchovies a:hover'; $store_rule = $new_pizza_store->add_rule( $selector ); @@ -126,33 +168,7 @@ public function test_get_all_rules() { $selector => $store_rule, ); - $this->assertEquals( $expected, $new_pizza_store->get_all_rules() ); - - $pizza_declarations = array( - 'color' => 'red', - 'border-color' => 'yellow', - 'border-radius' => '10rem', - ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $pizza_declarations ); - $store_rule->add_declarations( array( $css_declarations ) ); - - $expected = array( - $selector => $store_rule, - ); - $this->assertEquals( $expected, $new_pizza_store->get_all_rules() ); - - $new_pizza_declarations = array( - 'color' => 'red', - 'border-color' => 'red', - 'font-size' => '10rem', - ); - $css_declarations = new WP_Style_Engine_CSS_Declarations( $new_pizza_declarations ); - $store_rule->add_declarations( array( $css_declarations ) ); - - $expected = array( - $selector => $store_rule, - ); - $this->assertEquals( $expected, $new_pizza_store->get_all_rules() ); + $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations.' ); $new_selector = '.wp-block-mushroom a:hover'; $newer_pizza_declarations = array( @@ -166,6 +182,7 @@ public function test_get_all_rules() { $selector => $store_rule, $new_selector => $new_store_rule, ); - $this->assertEquals( $expected, $new_pizza_store->get_all_rules() ); + + $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.' ); } } 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 aca1efa21c671..d900e615c0776 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 @@ -1,23 +1,44 @@ add_declarations( array( @@ -35,16 +56,19 @@ public function test_return_rules_as_css() { ); $a_nice_processor = new WP_Style_Engine_Processor(); $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); - $this->assertEquals( + + $this->assertSame( '.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( array( 'prettify' => false ) ) ); } /** - * Should compile CSS rules. + * Tests compiling CSS rules and formatting them with new lines and indents. + * + * @covers ::get_css */ - public function test_return_prettified_rules_as_css() { + public function test_should_return_prettified_css_rules() { $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-wonderful-rule' ); $a_wonderful_css_rule->add_declarations( array( @@ -81,16 +105,18 @@ public function test_return_prettified_rules_as_css() { background-color: orange; } '; - $this->assertEquals( + $this->assertSame( $expected, $a_wonderful_processor->get_css( array( 'prettify' => true ) ) ); } /** - * Should compile CSS rules from the store. + * Tests adding a store and compiling CSS rules from that store. + * + * @covers ::add_store */ - public function test_return_store_rules_as_css() { + public function test_should_return_store_rules_as_css() { $a_nice_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'nice' ); $a_nice_store->add_rule( '.a-nice-rule' )->add_declarations( array( @@ -107,16 +133,19 @@ public function test_return_store_rules_as_css() { ); $a_nice_renderer = new WP_Style_Engine_Processor(); $a_nice_renderer->add_store( $a_nice_store ); - $this->assertEquals( + $this->assertSame( '.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( array( 'prettify' => false ) ) ); } /** - * Should merge CSS declarations. + * Tests that CSS declarations are merged and deduped in the final CSS rules output. + * + * @covers ::add_rules + * @covers ::get_css */ - public function test_dedupe_and_merge_css_declarations() { + public function test_should_dedupe_and_merge_css_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( @@ -136,9 +165,10 @@ public function test_dedupe_and_merge_css_declarations() { ) ); $an_excellent_processor->add_rules( $another_excellent_rule ); - $this->assertEquals( + $this->assertSame( '.an-excellent-rule{color:var(--excellent-color);border-style:dotted;border-color:brown;}', - $an_excellent_processor->get_css( array( 'prettify' => false ) ) + $an_excellent_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations with new, deduped and merged declarations.' ); $yet_another_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); @@ -150,16 +180,19 @@ public function test_dedupe_and_merge_css_declarations() { ) ); $an_excellent_processor->add_rules( $yet_another_excellent_rule ); - $this->assertEquals( + $this->assertSame( '.an-excellent-rule{color:var(--excellent-color);border-style:dashed;border-color:brown;border-width:2px;}', - $an_excellent_processor->get_css( array( 'prettify' => false ) ) + $an_excellent_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations with deduped and merged declarations.' ); } /** - * Should print out uncombined selectors duplicate CSS rules. + * Tests printing out 'unoptimized' CSS, that is, uncombined selectors and duplicate CSS rules. + * + * @covers ::get_css */ - public function test_output_verbose_css_rules() { + public function test_should_not_optimize_css_output() { $a_sweet_rule = new WP_Style_Engine_CSS_Rule( '.a-sweet-rule', array( @@ -187,7 +220,7 @@ public function test_output_verbose_css_rules() { $a_sweet_processor = new WP_Style_Engine_Processor(); $a_sweet_processor->add_rules( array( $a_sweet_rule, $a_sweeter_rule, $the_sweetest_rule ) ); - $this->assertEquals( + $this->assertSame( '.a-sweet-rule{color:var(--sweet-color);background-color:purple;}#an-even-sweeter-rule > marquee{color:var(--sweet-color);background-color:purple;}.the-sweetest-rule-of-all a{color:var(--sweet-color);background-color:purple;}', $a_sweet_processor->get_css( array( @@ -199,9 +232,11 @@ public function test_output_verbose_css_rules() { } /** - * Should combine duplicate CSS rules. + * Tests that 'optimized' CSS is output, that is, that duplicate CSS rules are combined under their corresponding selectors. + * + * @covers ::get_css */ - public function test_combine_css_rules() { + public function test_should_optimize_css_output_by_default() { $a_sweet_rule = new WP_Style_Engine_CSS_Rule( '.a-sweet-rule', array( @@ -221,15 +256,18 @@ public function test_combine_css_rules() { $a_sweet_processor = new WP_Style_Engine_Processor(); $a_sweet_processor->add_rules( array( $a_sweet_rule, $a_sweeter_rule ) ); - $this->assertEquals( + $this->assertSame( '.a-sweet-rule,#an-even-sweeter-rule > marquee{color:var(--sweet-color);background-color:purple;}', $a_sweet_processor->get_css( array( 'prettify' => false ) ) ); } - /** - * Should combine and store CSS rules. - */ - public function test_combine_previously_added_css_rules() { + + /** + * Tests that incoming CSS rules are merged with existing CSS rules. + * + * @covers ::add_rules + */ + public function test_should_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', @@ -245,7 +283,11 @@ public function test_combine_previously_added_css_rules() { ) ); $a_lovely_processor->add_rules( $a_lovelier_rule ); - $this->assertEquals( '.a-lovely-rule,.a-lovelier-rule{border-color:purple;}', $a_lovely_processor->get_css( array( 'prettify' => false ) ) ); + $this->assertSame( + '.a-lovely-rule,.a-lovelier-rule{border-color:purple;}', + $a_lovely_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations when combining 2 CSS rules' + ); $a_most_lovely_rule = new WP_Style_Engine_CSS_Rule( '.a-most-lovely-rule', @@ -263,9 +305,10 @@ public function test_combine_previously_added_css_rules() { ); $a_lovely_processor->add_rules( $a_perfectly_lovely_rule ); - $this->assertEquals( + $this->assertSame( '.a-lovely-rule,.a-lovelier-rule,.a-most-lovely-rule,.a-perfectly-lovely-rule{border-color:purple;}', - $a_lovely_processor->get_css( array( 'prettify' => false ) ) + $a_lovely_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations when combining 4 CSS rules' ); } } diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/style-engine-test.php similarity index 83% rename from packages/style-engine/phpunit/class-wp-style-engine-test.php rename to packages/style-engine/phpunit/style-engine-test.php index 9544204d29c74..98c833359474a 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/style-engine-test.php @@ -1,49 +1,84 @@ |`, to var( --wp--preset--* ) values. Default `false`. + * @type string $selector Optional. When a selector is passed, the value of `$css` in the return value will comprise a full CSS rule `$selector { ...$css_declarations }`, + * otherwise, the value will be a concatenated string of CSS declarations. + * } * @param string $expected_output The expected output. */ - public function test_generate_get_styles( $block_styles, $options, $expected_output ) { + public function test_wp_style_engine_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 for test_generate_get_styles(). + * Data provider for test_wp_style_engine_get_styles(). * * @return array */ - public function data_get_styles_fixtures() { + public function data_wp_style_engine_get_styles() { return array( 'default_return_value' => array( 'block_styles' => array(), @@ -111,28 +146,6 @@ public function data_get_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', - ), - ), - 'inline_valid_box_model_style' => array( 'block_styles' => array( 'spacing' => array( @@ -494,8 +507,11 @@ public function data_get_styles_fixtures() { /** * Tests adding rules to a store and retrieving a generated stylesheet. + * + * @covers ::wp_style_engine_get_styles + * @covers WP_Style_Engine::store_css_rule */ - public function test_store_block_styles_using_context() { + public function test_should_store_block_styles_using_context() { $block_styles = array( 'spacing' => array( 'padding' => array( @@ -516,13 +532,16 @@ public function test_store_block_styles_using_context() { ); $store = WP_Style_Engine::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. + * Tests that passing no context does not store styles. + * + * @covers ::wp_style_engine_get_styles */ - public function test_does_not_store_block_styles_without_context() { + public function test_should_not_store_block_styles_without_context() { $block_styles = array( 'typography' => array( 'fontSize' => '999px', @@ -536,15 +555,17 @@ public function test_does_not_store_block_styles_without_context() { ) ); - $all_stores = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_stores(); + $all_stores = WP_Style_Engine_CSS_Rules_Store::get_stores(); $this->assertEmpty( $all_stores ); } /** * Tests adding rules to a store and retrieving a generated stylesheet. + * + * @covers ::wp_style_engine_get_stylesheet_from_context */ - public function test_add_to_store() { + public function test_should_get_stored_stylesheet_from_context() { $css_rules = array( array( 'selector' => '.frodo', @@ -572,16 +593,16 @@ public function test_add_to_store() { ) ); - // Check that the style engine knows about the store. - $stored_store = WP_Style_Engine::get_store( 'test-store' ); - $this->assertInstanceOf( 'WP_Style_Engine_CSS_Rules_Store', $stored_store ); - $this->assertSame( $compiled_stylesheet, WP_Style_Engine::compile_stylesheet_from_css_rules( $stored_store->get_all_rules() ) ); + $this->assertSame( $compiled_stylesheet, wp_style_engine_get_stylesheet_from_context( 'test-store' ) ); } /** - * Tests retrieving a generated stylesheet from any rules. + * Tests returning a generated stylesheet from a set of rules. + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + * @covers WP_Style_Engine::compile_stylesheet_from_css_rules */ - public function test_get_stylesheet_from_css_rules() { + public function test_should_return_stylesheet_from_css_rules() { $css_rules = array( array( 'selector' => '.saruman', @@ -613,13 +634,17 @@ public function test_get_stylesheet_from_css_rules() { ); $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + $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. + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + * @covers WP_Style_Engine::compile_stylesheet_from_css_rules */ - public function test_get_deduped_and_merged_stylesheet() { + public function test_should_dedupe_and_merge_css_rules() { $css_rules = array( array( 'selector' => '.gandalf', @@ -657,6 +682,7 @@ public function test_get_deduped_and_merged_stylesheet() { ); $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + $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 ); } } diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts index 9c706981d88c2..64e4e4a6edeb9 100644 --- a/packages/style-engine/src/index.ts +++ b/packages/style-engine/src/index.ts @@ -17,10 +17,12 @@ import { styleDefinitions } from './styles'; /** * Generates a stylesheet for a given style object and selector. * + * @since 6.1.0 Introduced in WordPress core. + * * @param style Style object, for example, the value of a block's attributes.style object or the top level styles in theme.json * @param options Options object with settings to adjust how the styles are generated. * - * @return generated stylesheet. + * @return A generated stylesheet or inline style declarations. */ export function compileCSS( style: Style, options: StyleOptions = {} ): string { const rules = getCSSRules( style, options ); @@ -56,10 +58,12 @@ export function compileCSS( style: Style, options: StyleOptions = {} ): string { /** * Returns a JSON representation of the generated CSS rules. * + * @since 6.1.0 Introduced in WordPress core. + * * @param style Style object, for example, the value of a block's attributes.style object or the top level styles in theme.json * @param options Options object with settings to adjust how the styles are generated. * - * @return generated styles. + * @return A collection of objects containing the selector, if any, the CSS property key (camelcase) and parsed CSS value. */ export function getCSSRules( style: Style, diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php new file mode 100644 index 0000000000000..c7a5d5a71428c --- /dev/null +++ b/packages/style-engine/style-engine.php @@ -0,0 +1,154 @@ + array( 'text' => '#cccccc' ) ) ); + * // Returns `array( 'css' => 'color: #cccccc', 'declarations' => array( 'color' => '#cccccc' ), 'classnames' => 'has-color' )`. + * + * @since 6.1.0 + * + * @param array $block_styles The style object. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type string|null $context An identifier describing the origin of the style object, e.g., 'block-supports' or 'global-styles'. Default is `null`. + * When set, the style engine will attempt to store the CSS rules, where a selector is also passed. + * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset||`, to var( --wp--preset--* ) values. Default `false`. + * @type string $selector Optional. When a selector is passed, the value of `$css` in the return value will comprise a full CSS rule `$selector { ...$css_declarations }`, + * otherwise, the value will be a concatenated string of CSS declarations. + * } + * + * @return array { + * @type string $css A CSS ruleset or declarations block formatted to be placed in an HTML `style` attribute or tag. + * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * @type string $classnames Classnames separated by a space. + * } + */ +function wp_style_engine_get_styles( $block_styles, $options = array() ) { + $options = wp_parse_args( + $options, + array( + 'selector' => null, + 'context' => null, + 'convert_vars_to_classnames' => false, + ) + ); + + $parsed_styles = WP_Style_Engine::parse_block_styles( $block_styles, $options ); + + // Output. + $styles_output = array(); + + if ( ! empty( $parsed_styles['declarations'] ) ) { + $styles_output['css'] = WP_Style_Engine::compile_css( $parsed_styles['declarations'], $options['selector'] ); + $styles_output['declarations'] = $parsed_styles['declarations']; + if ( ! empty( $options['context'] ) ) { + WP_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 ); +} + +/** + * Returns compiled CSS from a collection of selectors and declarations. + * Useful for returning a compiled stylesheet from any collection of CSS selector + declarations. + * + * Example usage: + * $css_rules = array( array( 'selector' => '.elephant-are-cool', 'declarations' => array( 'color' => 'gray', 'width' => '3em' ) ) ); + * $css = wp_style_engine_get_stylesheet_from_css_rules( $css_rules ); + * // Returns `.elephant-are-cool{color:gray;width:3em}`. + * + * @since 6.1.0 + * + * @param array $css_rules { + * Required. A collection of CSS rules. + * + * @type array ...$0 { + * @type string $selector A CSS selector. + * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * } + * } + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type string|null $context An identifier describing the origin of the style object, e.g., 'block-supports' or 'global-styles'. Default is 'block-supports'. + * When set, the style engine will attempt to store the CSS rules. + * @type bool $optimize Whether to optimize the CSS output, e.g., combine rules. Default is `false`. + * @type bool $prettify Whether to add new lines and indents to output. Default is the test of whether the global constant `SCRIPT_DEBUG` is defined. + * } + * + * @return string A string of compiled CSS declarations, or empty string. + */ +function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = array() ) { + if ( empty( $css_rules ) ) { + return ''; + } + + $options = wp_parse_args( + $options, + array( + 'context' => null, + ) + ); + + $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; + } + + if ( ! empty( $options['context'] ) ) { + WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'] ); + } + + $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, $options ); +} + +/** + * Returns compiled CSS from a store, if found. + * + * @since 6.1.0 + * + * @param string $context A valid context name, corresponding to an existing store key. + * @param array $options { + * Optional. An array of options. Default empty array. + * + * @type bool $optimize Whether to optimize the CSS output, e.g., combine rules. Default is `false`. + * @type bool $prettify Whether to add new lines and indents to output. Default is the test of whether the global constant `SCRIPT_DEBUG` is defined. + * } + * + * @return string A compiled CSS string. + */ +function wp_style_engine_get_stylesheet_from_context( $context, $options = array() ) { + if ( empty( $context ) ) { + return ''; + } + + return WP_Style_Engine::compile_stylesheet_from_css_rules( WP_Style_Engine::get_store( $context )->get_all_rules(), $options ); +} diff --git a/phpunit/script-loader.php b/phpunit/script-loader.php index b8dfa970b1762..bb11f6cd60db3 100644 --- a/phpunit/script-loader.php +++ b/phpunit/script-loader.php @@ -13,7 +13,7 @@ class WP_Script_Loader_Test extends WP_UnitTestCase { * @global WP_Styles $wp_styles */ public function clean_up_global_scope() { - global $wp_styles; + global $wp_styles; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable parent::clean_up_global_scope(); $wp_styles = null; } @@ -21,8 +21,6 @@ public function clean_up_global_scope() { * Tests that stored CSS is enqueued. */ public function test_enqueue_stored_styles() { - global $wp_styles; - $core_styles_to_enqueue = array( array( 'selector' => '.saruman', @@ -39,7 +37,6 @@ public function test_enqueue_stored_styles() { $core_styles_to_enqueue, array( 'context' => 'block-supports', - 'enqueue' => true, ) ); @@ -59,13 +56,21 @@ public function test_enqueue_stored_styles() { $my_styles_to_enqueue, array( 'context' => 'my-styles', - 'enqueue' => true, ) ); gutenberg_enqueue_stored_styles(); - $this->assertEquals( array( '.saruman{color:white;height:100px;border-style:solid;}' ), $wp_styles->get_data( 'core-block-supports', 'after' ) ); - $this->assertEquals( array( '.gandalf{color:grey;height:90px;border-style:dotted;}' ), $wp_styles->get_data( 'wp-style-engine-my-styles', 'after' ) ); + $this->assertEquals( + array( '.saruman{color:white;height:100px;border-style:solid;}' ), + wp_styles()->registered['core-block-supports']->extra['after'], + 'Registered styles with handle of "core-block-supports" do not match expected value from Style Engine store.' + ); + + $this->assertEquals( + array( '.gandalf{color:grey;height:90px;border-style:dotted;}' ), + wp_styles()->registered['wp-style-engine-my-styles']->extra['after'], + 'Registered styles with handle of "wp-style-engine-my-styles" do not match expected value from the Style Engine store.' + ); } } diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 7d463a78258c7..3159f4a4cc426 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -23,11 +23,7 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; // Experimental or other packages that should be private are bundled when used. // That way, we can iterate on these package without making them part of the public API. // See: https://github.com/WordPress/gutenberg/pull/19809 -const BUNDLED_PACKAGES = [ - '@wordpress/icons', - '@wordpress/interface', - '@wordpress/style-engine', -]; +const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; // PHP files in packages that have to be copied during build. const bundledPackagesPhpConfig = [ @@ -118,7 +114,6 @@ const vendorsCopyConfig = Object.entries( vendors ).flatMap( ]; } ); - module.exports = { ...baseConfig, name: 'packages',