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: output global styles CSS rules using selectors #40955

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,23 +491,25 @@ function( $pseudo_selector ) use ( $selector ) {
// If the current selector is a pseudo selector that's defined in the allow list for the current
// element then compute the style properties for it.
// Otherwise just compute the styles for the default selector as normal.
if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) {
$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json );
} else {
$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json );
}

// if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) {
// $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json );
// } else {
// $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json );
// }

$block_rules = '';

// @TODO migrate duotone to style engine
// 1. Separate the ones who use the general selector
// and the ones who use the duotone selector.
$declarations_duotone = array();
foreach ( $declarations as $index => $declaration ) {
if ( 'filter' === $declaration['name'] ) {
unset( $declarations[ $index ] );
$declarations_duotone[] = $declaration;
}
}
// $declarations_duotone = array();
// foreach ( $declarations as $index => $declaration ) {
// if ( 'filter' === $declaration['name'] ) {
// unset( $declarations[ $index ] );
// $declarations_duotone[] = $declaration;
// }
// }

/*
* Reset default browser margin on the root body element.
Expand All @@ -522,14 +524,41 @@ function( $pseudo_selector ) use ( $selector ) {
}

// 2. Generate and append the rules that use the general selector.
$block_rules .= static::to_ruleset( $selector, $declarations );
//$block_rules .= static::to_ruleset( $selector, $declarations );

$block_styles = $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ? $node[ $pseudo_selector ] : $node;

if ( isset( $block_styles['filter']['duotone'] ) && ! empty( $block_styles['filter']['duotone'] ) ) {
$selector = static::scope_selector( $selector, $block_metadata['duotone'] );
}

$styles = gutenberg_style_engine_generate(
$block_styles,
array(
'selector' => $selector,
'custom_metadata' => array(
Copy link
Member Author

Choose a reason for hiding this comment

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

Taking some inspiration from #41965

'spacing.blockGap' => array(
'property_keys' => array(
'default' => '--wp--style--block-gap',
),
),
),
'css_vars' => true, // @TODO this doesn't make sense in this context. Refactor.
'prettify' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
)
);

// 3. Generate and append the rules that use the duotone selector.
if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
$selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
$block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone );
if ( isset( $styles['css'] ) ) {
$block_rules .= $styles['css'];
}

// 3. Generate and append the rules that use the duotone selector.
// @TODO migrate duotone to style engine
// if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
// $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
// $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone );
// }

if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
$block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
$block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
Expand Down
19 changes: 16 additions & 3 deletions packages/style-engine/class-wp-style-engine-css-declarations.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ class WP_Style_Engine_CSS_Declarations {
protected $styles = array();

/**
* Contructor for this object.
* A white list of CSS custom properties.
* Used to bypass safecss_filter_attr().
*/
const VALID_CUSTOM_PROPERTIES = array( '--wp--style--block-gap' => 'blockGap' );

/**
* Constructor for this object.
*
* If a `$styles` array is passed, it will be used to populate
* the initial $styles prop of the object by calling add_declarations().
Expand Down Expand Up @@ -92,15 +98,22 @@ public function get_styles() {
/**
* Get the CSS styles.
*
* @param boolean $should_prettify Whether to format the output with indents.
*
* @return string The CSS styles.
*/
public function get_styles_string() {
public function get_styles_string( $should_prettify = false ) {
$styles_array = $this->get_styles();
$styles = '';
$indent = $should_prettify ? "\t" : '';
$newline = $should_prettify ? "\n" : ' ';
foreach ( $styles_array as $property => $value ) {
$css = esc_html( safecss_filter_attr( "{$property}: {$value}" ) );
// @TODO The safecss_filter_attr_allow_css filter in safecss_filter_attr (kses.php) does not let through CSS custom variables.
// So we have to be strict about them here.
$css = isset( static::VALID_CUSTOM_PROPERTIES[ $property ] ) ? esc_html( "{$property}: {$value}" ) : esc_html( safecss_filter_attr( "{$property}: {$value}" ) );
if ( $css ) {
$styles .= $css . '; ';
$styles .= "$indent$css;$newline";
}
}
return rtrim( $styles );
Expand Down
96 changes: 74 additions & 22 deletions packages/style-engine/class-wp-style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class WP_Style_Engine {
'default' => 'background-color',
),
'path' => array( 'color', 'background' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
'classnames' => array(
'has-background' => true,
'has-$slug-background-color' => 'color',
Expand Down Expand Up @@ -141,8 +144,16 @@ class WP_Style_Engine {
),
),
),
'filter' => array(
'duotone' => array(
'property_keys' => array(
'default' => 'filter',
),
'path' => array( 'filter', 'duotone' ),
),
),
'spacing' => array(
'padding' => array(
'padding' => array(
'property_keys' => array(
'default' => 'padding',
'individual' => 'padding-%s',
Expand All @@ -152,7 +163,7 @@ class WP_Style_Engine {
'spacing' => '--wp--preset--spacing--$slug',
),
),
'margin' => array(
'margin' => array(
'property_keys' => array(
'default' => 'margin',
'individual' => 'margin-%s',
Expand All @@ -162,6 +173,15 @@ class WP_Style_Engine {
'spacing' => '--wp--preset--spacing--$slug',
),
),
'blockGap' => array(
'property_keys' => array(
// @TODO 'grid-gap' has been deprecated in favor of 'gap'.
// See: https://developer.mozilla.org/en-US/docs/Web/CSS/gap.
// Update the white list in safecss_filter_attr (kses.php).
'default' => 'grid-gap',
),
'path' => array( 'spacing', 'blockGap' ),
),
),
'typography' => array(
'fontSize' => array(
Expand Down Expand Up @@ -236,6 +256,25 @@ public static function get_instance() {
return self::$instance;
}

/**
* 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 The merged definition metadata.
*/
public static function merge_custom_style_definitions_metadata( $style_definition, $custom_definition = array() ) {
$valid_keys = array( 'property_keys', 'classnames' );
foreach ( $valid_keys as $key ) {
if ( isset( $custom_definition[ $key ] ) && is_array( $custom_definition[ $key ] ) ) {
$style_definition[ $key ] = array_merge( $style_definition[ $key ], $custom_definition[ $key ] );
}
}

return $style_definition;
}

/**
* Extracts the slug in kebab case from a preset string, e.g., "heavenly-blue" from 'var:preset|color|heavenlyBlue'.
*
Expand Down Expand Up @@ -332,29 +371,30 @@ 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<string> $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
* @param boolean $should_skip_css_vars Whether to skip compiling CSS var values.
* @param array $style_value A single raw style value from the generate() $block_styles array.
* @param array<string> $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
* @param array<string> $options Options passed to generate().
*
* @return array An array of CSS definitions, e.g., array( "$property" => "$value" ).
*/
protected static function get_css_declarations( $style_value, $style_definition, $should_skip_css_vars = false ) {
protected static function get_css_declarations( $style_value, $style_definition, $options ) {
if (
isset( $style_definition['value_func'] ) &&
is_callable( $style_definition['value_func'] )
) {
return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $should_skip_css_vars );
return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options );
}

$css_declarations = array();
$style_property_keys = $style_definition['property_keys'];
$css_declarations = array();
$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.
if ( is_string( $style_value ) && strpos( $style_value, 'var:' ) !== false ) {
if ( ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) {
$css_var = static::get_css_var_value( $style_value, $style_definition['css_vars'] );
if ( $css_var ) {
if ( $css_var && isset( $style_property_keys['default'] ) ) {
$css_declarations[ $style_property_keys['default'] ] = $css_var;
}
}
Expand All @@ -366,6 +406,9 @@ protected static function get_css_declarations( $style_value, $style_definition,
// for styles such as margins and padding.
if ( is_array( $style_value ) ) {
foreach ( $style_value as $key => $value ) {
if ( ! isset( $style_property_keys['individual'] ) ) {
return $css_declarations;
}
if ( is_string( $value ) && strpos( $value, 'var:' ) !== false && ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) {
$value = static::get_css_var_value( $value, $style_definition['css_vars'] );
}
Expand All @@ -374,7 +417,7 @@ protected static function get_css_declarations( $style_value, $style_definition,
$css_declarations[ $individual_property ] = $value;
}
}
} else {
} elseif ( isset( $style_property_keys['default'] ) ) {
$css_declarations[ $style_property_keys['default'] ] = $style_value;
}

Expand All @@ -401,41 +444,49 @@ public function generate( $block_styles, $options ) {
return null;
}

$css_declarations = array();
$classnames = array();
$should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames'];
$css_declarations = array();
$classnames = array();
$custom_metadata = isset( $options['custom_metadata'] ) && is_array( $options['custom_metadata'] ) ? $options['custom_metadata'] : null;

// Collect CSS and classnames.
foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) {
if ( empty( $block_styles[ $definition_group_key ] ) ) {
continue;
}
foreach ( $definition_group_style as $style_definition ) {
foreach ( $definition_group_style as $style_key => $style_definition ) {
// Merge incoming custom metadata.
if ( isset( $custom_metadata[ "$definition_group_key.$style_key" ] ) ) {
$style_definition = static::merge_custom_style_definitions_metadata( $style_definition, $custom_metadata[ "$definition_group_key.$style_key" ] );
}
$style_value = _wp_array_get( $block_styles, $style_definition['path'], null );

if ( ! static::is_valid_style_value( $style_value ) ) {
continue;
}

$classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) );
$css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $should_skip_css_vars ) );
$css_declarations = array_merge( $css_declarations, static::get_css_declarations( $style_value, $style_definition, $options ) );
}
}

// Build CSS rules output.
$css_selector = isset( $options['selector'] ) ? $options['selector'] : null;
$style_rules = new WP_Style_Engine_CSS_Declarations( $css_declarations );
$css_selector = isset( $options['selector'] ) ? $options['selector'] : null;
$should_prettify = isset( $options['prettify'] ) ? $options['prettify'] : null;
$style_rules = new WP_Style_Engine_CSS_Declarations( $css_declarations );

// The return object.
$styles_output = array();
$css = $style_rules->get_styles_string();
$css = $style_rules->get_styles_string( $should_prettify );

// Return css, if any.
if ( ! empty( $css ) ) {
$styles_output['css'] = $css;
// Return an entire rule if there is a selector.
if ( $css_selector ) {
$styles_output['css'] = $css_selector . ' { ' . $css . ' }';
$new_line = $should_prettify ? "\n" : '';
$spacer = $should_prettify ? '' : ' ';
// @TODO this is just weird. Simplify?
$styles_output['css'] = "$css_selector {{$spacer}{$new_line}{$css}{$new_line}{$spacer}}$new_line";
}
}

Expand All @@ -456,17 +507,18 @@ public function generate( $block_styles, $options ) {
*
* @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 boolean $should_skip_css_vars Whether to skip compiling CSS var values.
* @param array<string> $options Options passed to generate().
*
* @return array An array of CSS definitions, e.g., array( "$property" => "$value" ).
*/
protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $should_skip_css_vars ) {
protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options ) {
$css_declarations = array();

if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) {
return $css_declarations;
}

$should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames'];
// 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".
Expand Down
Loading