diff --git a/lib/load.php b/lib/load.php index ef8da334debe6e..001b1fa5ffb89e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -215,6 +215,9 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-store-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-processor-gutenberg.php'; } +if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-block-style-metadata-gutenberg.php' ) ) { + require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-block-style-metadata-gutenberg.php'; +} // Block supports overrides. require __DIR__ . '/block-supports/settings.php'; diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index 48030434c19496..1de7c41282697a 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancement + +- Style engine: provide a way to extend default block support style definitions [#45296](https://github.com/WordPress/gutenberg/pull/45296) + ## 1.24.0 (2023-08-31) ## 1.23.0 (2023-08-16) diff --git a/packages/style-engine/class-wp-style-engine-block-style-metadata.php b/packages/style-engine/class-wp-style-engine-block-style-metadata.php new file mode 100644 index 00000000000000..ac7d9e9f45680e --- /dev/null +++ b/packages/style-engine/class-wp-style-engine-block-style-metadata.php @@ -0,0 +1,150 @@ +base_metadata = $base_metadata; + $this->reset_metadata(); + } + + /** + * Adds block style metadata. + * + * @param array $new_metadata The $metadata to be added. + * + * @return WP_Style_Engine_Block_Style_Metadata Returns the object to allow chaining methods. + */ + public function add_metadata( $new_metadata = array() ) { + if ( empty( $new_metadata ) ) { + return $this; + } + + foreach ( $new_metadata as $definition_group_key => $definition_group_style ) { + if ( ! is_array( $definition_group_style ) || empty( $definition_group_style ) ) { + continue; + } + + // Adds a new top-level group if it doesn't exist already. + if ( ! isset( $this->merged_block_support_metadata[ $definition_group_key ] ) ) { + $this->merged_block_support_metadata[ $definition_group_key ] = array(); + } + + foreach ( $definition_group_style as $style_definition_key => $style_definition ) { + // Bails early if merging metadata is attempting to overwrite existing, original style metadata. + if ( isset( $this->base_metadata[ $definition_group_key ] ) + && isset( $this->base_metadata[ $definition_group_key ][ $style_definition_key ] ) ) { + continue; + } + + if ( ! is_array( $style_definition ) || empty( $style_definition ) ) { + continue; + } + + $array_to_extend = isset( $this->merged_block_support_metadata[ $definition_group_key ][ $style_definition_key ] ) + ? $this->merged_block_support_metadata[ $definition_group_key ][ $style_definition_key ] : array(); + $merged_style_definition = $this->merge_custom_style_definitions_metadata( $array_to_extend, $style_definition ); + + if ( $merged_style_definition ) { + $this->merged_block_support_metadata[ $definition_group_key ][ $style_definition_key ] = $merged_style_definition; + } + } + } + return $this; + } + + /** + * Returns merged metadata. + * + * @param array $path A path to an array item in static::$merged_block_support_metadata. + * @return array + */ + public function get_metadata( $path = array() ) { + if ( ! empty( $path ) ) { + return _wp_array_get( $this->merged_block_support_metadata, $path, null ); + } + return $this->merged_block_support_metadata; + } + + /** + * Resets the de-referenced metadata array. + * + * @return void + */ + public function reset_metadata() { + $this->merged_block_support_metadata = json_decode( wp_json_encode( $this->base_metadata ), true ); + } + + /** + * Merges single style definitions with incoming custom style definitions. + * + * @param array $style_definition The internal style definition metadata. + * @param array $custom_definition The custom style definition metadata to be merged. + * + * @return array|void The merged definition metadata. + */ + protected function merge_custom_style_definitions_metadata( $style_definition, $custom_definition = array() ) { + // Required metadata. + if ( ! isset( $style_definition['path'] ) && ! isset( $custom_definition['path'] ) && ! is_array( $custom_definition['path'] ) ) { + return; + } + + // Only allow strings for valid property keys. + if ( ! isset( $custom_definition['property_keys']['default'] ) && ! is_string( $custom_definition['property_keys']['default'] ) ) { + return; + } + + // Only allow strings for valid property keys. + if ( isset( $custom_definition['property_keys']['individual'] ) && ! is_string( $custom_definition['property_keys']['individual'] ) ) { + return; + } + + $custom_definition['property_keys']['default'] = sanitize_key( $custom_definition['property_keys']['default'] ); + + // A white list of keys that may be merged. Note the absence of the callable `value_func`. + $valid_keys = array( 'path', 'property_keys', 'css_vars', 'classnames' ); + foreach ( $valid_keys as $key ) { + if ( isset( $custom_definition[ $key ] ) && is_array( $custom_definition[ $key ] ) ) { + if ( ! isset( $style_definition[ $key ] ) ) { + $style_definition[ $key ] = array(); + } + $style_definition[ $key ] = array_merge( $style_definition[ $key ], $custom_definition[ $key ] ); + } + } + + return $style_definition; + } +} diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index 283601e551c98f..036c7f27853350 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -364,7 +364,7 @@ public static function parse_block_styles( $block_styles, $options ) { } // Collect CSS and classnames. - foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) { + foreach ( $options['metadata'] as $definition_group_key => $definition_group_style ) { if ( empty( $block_styles[ $definition_group_key ] ) ) { continue; } @@ -527,7 +527,7 @@ protected static function get_individual_property_css_declarations( $style_value // Build a path to the individual rules in definitions. $style_definition_path = array( $definition_group_key, $css_property ); - $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition_path, null ); + $style_definition = _wp_array_get( $options['metadata'], $style_definition_path, null ); if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { // Set a CSS var if there is a valid preset value. diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php index 4571a2fcce4ffe..ce0047a44dcc04 100644 --- a/packages/style-engine/style-engine.php +++ b/packages/style-engine/style-engine.php @@ -30,6 +30,7 @@ * @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. + * @type array $metadata An associate array in the format of WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA that extends the latter. * } * * @return array { @@ -45,10 +46,13 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { 'selector' => null, 'context' => null, 'convert_vars_to_classnames' => false, + 'metadata' => array(), ) ); - $parsed_styles = WP_Style_Engine::parse_block_styles( $block_styles, $options ); + $block_style_metadata = new WP_Style_Engine_Block_Style_Metadata( WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA ); + $options['metadata'] = $block_style_metadata->add_metadata( $options['metadata'] )->get_metadata(); + $parsed_styles = WP_Style_Engine::parse_block_styles( $block_styles, $options ); // Output. $styles_output = array(); diff --git a/phpunit/style-engine/class-wp-style-engine-block-style-metadata-test.php b/phpunit/style-engine/class-wp-style-engine-block-style-metadata-test.php new file mode 100644 index 00000000000000..9e4218e6c2e463 --- /dev/null +++ b/phpunit/style-engine/class-wp-style-engine-block-style-metadata-test.php @@ -0,0 +1,176 @@ +get_metadata(); + $this->assertEquals( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA, $full_metadata, 'Returning all default definitions' ); + + $color_metadata = $block_style_metadata->get_metadata( array( 'color' ) ); + $this->assertEquals( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA['color'], $color_metadata, 'Returning top-level color definition' ); + + $color_metadata = $block_style_metadata->get_metadata( array( 'color', 'background' ) ); + $this->assertEquals( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA['color']['background'], $color_metadata, 'Returning second-level color > background definition' ); + + $null_metadata = $block_style_metadata->get_metadata( array( 'something', 'background' ) ); + $this->assertNull( $null_metadata, 'Returning `null` where the path is invalid' ); + } + + /** + * Tests adding metadata to the block styles definition. + * + * @covers ::add_metadata + */ + public function test_should_add_new_top_level_metadata() { + $block_style_metadata = new WP_Style_Engine_Block_Style_Metadata_Gutenberg( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA ); + $new_metadata = array( + 'layout' => array( + 'float' => array( + 'property_keys' => array( + 'default' => 'float', + ), + 'path' => array( 'layout', 'float' ), + 'css_vars' => array( + 'layout' => '--wp--preset--float--$slug', + ), + 'classnames' => array( + 'has-float-layout' => true, + 'has-$slug-float' => 'layout', + ), + ), + 'width' => array( + 'property_keys' => array( + 'default' => 'width', + 'individual' => '%s-width', + ), + 'path' => array( 'layout', 'width' ), + 'classnames' => array( + 'has-$slug-width' => 'layout', + ), + ), + ), + ); + $this->assertEquals( + $new_metadata['layout'], + $block_style_metadata->add_metadata( $new_metadata )->get_metadata( array( 'layout' ) ), + 'A new style definition for `layout` should be registered' + ); + } + + /** + * Tests adding new second-level property metadata to the block styles definition and ignore `value_func` values. + * + * @covers ::add_metadata + */ + public function test_should_add_new_style_property_metadata_keys() { + $block_style_metadata = new WP_Style_Engine_Block_Style_Metadata_Gutenberg( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA ); + $new_metadata = array( + 'typography' => array( + 'textIndent' => array( + 'property_keys' => array( + 'default' => 'text-indent', + ), + 'css_vars' => array( + 'spacing' => '--wp--preset--spacing--$slug', + ), + 'path' => array( 'typography', 'textIndent' ), + 'classnames' => array( + 'has-text-indent' => true, + ), + 'value_func' => 'Test::function', + ), + ), + ); + $block_style_metadata->add_metadata( $new_metadata ); + + // Remove ignored property keys. + unset( $new_metadata['typography']['textIndent']['value_func'] ); + + $this->assertEquals( + $new_metadata['typography']['textIndent'], + $block_style_metadata->get_metadata( array( 'typography', 'textIndent' ) ), + 'The new style property should match expected.' + ); + } + + /** + * Tests that merging style metadata to the block styles definitions does not work. + * + * @covers ::add_metadata + */ + public function test_should_not_overwrite_style_property_metadata() { + $block_style_metadata = new WP_Style_Engine_Block_Style_Metadata_Gutenberg( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA ); + $new_metadata = array( + 'spacing' => array( + 'padding' => array( + 'property_keys' => array( + 'default' => 'columns', + ), + 'css_vars' => array( + 'spacing' => '--wp--preset--column--$slug', + ), + ), + ), + ); + + $block_style_metadata->add_metadata( $new_metadata ); + $this->assertEquals( + WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA['spacing']['padding'], + $block_style_metadata->get_metadata( array( 'spacing', 'padding' ) ), + 'The newly-merged property metadata should be present' + ); + } + + /** + * Tests resetting metadata to the original block styles definition. + * + * @covers ::reset_metadata + */ + public function test_should_reset_metadata() { + $block_style_metadata = new WP_Style_Engine_Block_Style_Metadata_Gutenberg( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA ); + $new_metadata = array( + 'spacing' => array( + 'gap' => array( + 'property_keys' => array( + 'default' => 'gap', + 'individual' => 'gap-%', + ), + 'path' => array( 'spacing', 'gap' ), + 'css_vars' => array( + 'spacing' => '--wp--preset--spacing--$slug', + ), + ), + ), + ); + + $this->assertEquals( + $new_metadata['spacing']['gap'], + $block_style_metadata->add_metadata( $new_metadata )->get_metadata( array( 'spacing', 'gap' ) ), + 'Should successfully merge metadata' + ); + + $block_style_metadata->reset_metadata(); + $this->assertEquals( + WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA['spacing'], + $block_style_metadata->get_metadata( array( 'spacing' ) ), + 'Should be equal to original' + ); + } +} diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index fae95b995ee44d..279b051aa83ee2 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -493,6 +493,71 @@ public function data_wp_style_engine_get_styles() { ), ), ), + + 'extend_block_style_definitions_with_metadata' => array( + 'block_styles' => array( + 'layout' => array( + 'float' => 'var:preset|layout|left', + 'width' => array( + 'max' => '100px', + 'min' => '20px', + ), + ), + 'typography' => array( + 'textIndent' => '1rem', + ), + ), + 'options' => array( + 'metadata' => array( + 'layout' => array( + 'float' => array( + 'property_keys' => array( + 'default' => 'float', + ), + 'path' => array( 'layout', 'float' ), + 'css_vars' => array( + 'layout' => '--wp--preset--float--$slug', + ), + 'classnames' => array( + 'has-float-layout' => true, + 'has-$slug-float' => 'layout', + ), + ), + 'width' => array( + 'property_keys' => array( + 'default' => 'width', + 'individual' => '%s-width', + ), + 'path' => array( 'layout', 'width' ), + 'classnames' => array( + 'has-$slug-width' => 'layout', + ), + ), + ), + 'typography' => array( + 'textIndent' => array( + 'property_keys' => array( + 'default' => 'text-indent', + ), + 'path' => array( 'typography', 'textIndent' ), + 'classnames' => array( + 'has-text-indent' => true, + ), + ), + ), + ), + ), + 'expected_output' => array( + 'css' => 'text-indent:1rem;float:var(--wp--preset--float--left);max-width:100px;min-width:20px;', + 'declarations' => array( + 'text-indent' => '1rem', + 'float' => 'var(--wp--preset--float--left)', + 'max-width' => '100px', + 'min-width' => '20px', + ), + 'classnames' => 'has-text-indent has-float-layout has-left-float', + ), + ), ); } diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index e5bb74abdb0a1f..5f17c733a21616 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -32,6 +32,7 @@ const bundledPackagesPhpConfig = [ from: './packages/style-engine/', to: 'build/style-engine/', replaceClasses: [ + 'WP_Style_Engine_Block_Style_Metadata', 'WP_Style_Engine_CSS_Declarations', 'WP_Style_Engine_CSS_Rules_Store', 'WP_Style_Engine_CSS_Rule',