Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Style engine: extend block support style definitions #45296

Closed
wants to merge 10 commits into from
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 4 additions & 0 deletions packages/style-engine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
150 changes: 150 additions & 0 deletions packages/style-engine/class-wp-style-engine-block-style-metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
/**
* WP_Style_Engine_Block_Style_Metadata
*
* Stores block support metadata and associated rules.
*
* @package Gutenberg
*/

if ( class_exists( 'WP_Style_Engine_Block_Style_Metadata' ) ) {
return;
}

/**
* Stores block support metadata and associated rules.
*
* @access private
*/
class WP_Style_Engine_Block_Style_Metadata {
/**
* A variable to cache original metadata.
*
* @var array
*/
protected $base_metadata = array();

/**
* The merged metadata.
*
* @var array
*/
protected $merged_block_support_metadata = array();

/**
* Constructor for this object.
*
* @param array $base_metadata An associative array of block style metadata to extend.
*/
public function __construct( $base_metadata = array() ) {
$this->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;
}
}
4 changes: 2 additions & 2 deletions packages/style-engine/class-wp-style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion packages/style-engine/style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, e.g., `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`, 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 {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php
/**
* Tests the Style Engine Block Style Metadata class.
*
* @package Gutenberg
* @subpackage style-engine
*/

/**
* Tests fetching, extending and otherwise manipulation block supports style definitions.
*
* @coversDefaultClass WP_Style_Engine_Block_Style_Metadata_Gutenberg
*/
class WP_Style_Engine_Block_Style_Metadata_Test extends WP_UnitTestCase {
/**
* Tests getting metadata.
*
* @covers ::get_metadata
*/
public function test_should_get_metadata() {
$block_style_metadata = new WP_Style_Engine_Block_Style_Metadata_Gutenberg( WP_Style_Engine_Gutenberg::BLOCK_STYLE_DEFINITIONS_METADATA );

$full_metadata = $block_style_metadata->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'
);
}
}
Loading