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

Global Styles: Add Global Styles CSS to inline block CSS #40513

Closed
wants to merge 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* @access private
*/
class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 {
class WP_Theme_JSON_6_0 extends WP_Theme_JSON_5_9 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we should settle on a convention to only use the _Gutenberg suffix in the experimental folder, and all version specific classes should use the version suffix. That way if we want to inherit all the features in the plugin we don't need to specify the version of the class, we can just always use the _Gutenberg suffix. If we don't do this then we need to copy/paste even more code!

/**
* Metadata for style properties.
*
Expand Down
200 changes: 200 additions & 0 deletions lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php
/**
* WP_Theme_JSON_6_1 class
*
* @package gutenberg
*/

/**
* Class that encapsulates the processing of structures that adhere to the theme.json spec.
*
* 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 get_global_settings, get_global_styles, and get_global_stylesheet instead.
*
* @access private
*/
class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 {
/**
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
*
* @param array $types Types of styles to load. Will load all by default. It accepts:
* 'variables': only the CSS Custom Properties for presets & custom ones.
* 'styles': only the styles section in theme.json.
* 'presets': only the classes for the presets.
* @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
* @return string Stylesheet.
*/
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
}

if ( is_string( $types ) ) {
// Dispatch error and map old arguments to new ones.
_deprecated_argument( __FUNCTION__, '5.9' );
if ( 'block_styles' === $types ) {
$types = array( 'styles', 'presets' );
} elseif ( 'css_variables' === $types ) {
$types = array( 'variables' );
} else {
$types = array( 'variables', 'styles', 'presets' );
}
}

$blocks_metadata = static::get_blocks_metadata();
$separate_block_assets = wp_should_load_separate_core_block_assets();
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit of a red flag for me. This class was designed to avoid having "global state from WordPress" here: things like "has theme support" or "is this a classic or a block theme", etc.

In my view, this class should be focused on low-level primitives (transform a theme.json into a stylesheet, filtering, etc) and the "domain logic" should go elsewhere (resolver or the higher-level global styles functions). Things like testing or continue to modify its behavior is a lot harder if we don't.

$style_nodes_without_blocks = static::get_style_nodes( $this->theme_json, $blocks_metadata, $separate_block_assets );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be called $style_nodes_maybe_without_blocks, maybe?

Copy link
Member

@ramonjd ramonjd Apr 27, 2022

Choose a reason for hiding this comment

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

Or let the third argument do all the explaining? E.g., $should_exclude_block_nodes

$should_exclude_block_nodes      = wp_should_load_separate_core_block_assets();
$style_nodes                     = static::get_style_nodes( $this->theme_json, $blocks_metadata, $should_exclude_block_nodes );

$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

$stylesheet = '';

if ( in_array( 'variables', $types, true ) ) {
$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
}

if ( in_array( 'styles', $types, true ) ) {
$stylesheet .= $this->get_block_classes( $style_nodes_without_blocks );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is really the only change to this function, plus the lines that define this variable. The purpose is to ensure that we are only adding top-level CSS to the Global Styles block, rather than block specific CSS which is added elsewhere.

}

if ( in_array( 'presets', $types, true ) ) {
$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
}

return $stylesheet;
}

/**
* Builds metadata for the style nodes, which returns in the form of:
*
* [
* [
* 'path' => [ 'path', 'to', 'some', 'node' ],
* 'selector' => 'CSS selector for some node',
* 'duotone' => 'CSS selector for duotone for some node'
* ],
* [
* 'path' => ['path', 'to', 'other', 'node' ],
* 'selector' => 'CSS selector for other node',
* 'duotone' => null
* ],
* ]
*
* @since 5.8.0
*
* @param array $theme_json The tree to extract style nodes from.
* @param array $selectors List of selectors per block.
* @param array $exclude_blocks Exclude blocks from the style nodes.
* @return array
*/
protected static function get_style_nodes( $theme_json, $selectors = array(), $exclude_blocks = false ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The exclude blocks parameter is new and is used to determine whether to get all style nodes, or only the top level and elements nodes. We might prefer to create three new functions, one to get blocks, one to get top level and one for elements, and then combine them as needed.

$nodes = array();
if ( ! isset( $theme_json['styles'] ) ) {
return $nodes;
}

// Top-level.
$nodes[] = array(
'path' => array( 'styles' ),
'selector' => static::ROOT_BLOCK_SELECTOR,
);

if ( isset( $theme_json['styles']['elements'] ) ) {
foreach ( $theme_json['styles']['elements'] as $element => $node ) {
$nodes[] = array(
'path' => array( 'styles', 'elements', $element ),
'selector' => static::ELEMENTS[ $element ],
);
}
}

if ( $exclude_blocks ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the addition.

return $nodes;
}

// Blocks.
if ( ! isset( $theme_json['styles']['blocks'] ) ) {
return $nodes;
}

$nodes = array_merge( $nodes, static::get_block_nodes_private( $theme_json ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a refactor, moving this part of the code into a new function so it can be shared.


return $nodes;
}

/**
* A public helper to get the block nodes from a theme.json file.
*
* @return array The block nodes in theme.json.
*/
public function get_block_nodes() {
Copy link
Contributor Author

@scruffian scruffian Apr 21, 2022

Choose a reason for hiding this comment

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

This is a wrapper so that we can call this function as a method on the class as well as statically. I'm not sure this is a great idea.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, yeah I see we use exclusively public functions for the API, e.g., $tree->get_stylesheet() but we need to call the static function static::get_block_nodes_private() given to the get_style_nodes() is protected static (can't call public/private members).

I'm not sure there's an easy approach without a major refactor.

return static::get_block_nodes_private( $this->theme_json );
}

/**
* An internal method to get the block nodes from a theme.json file.
*
* @param array $theme_json The theme.json converted to an array.
*
* @return array The block nodes in theme.json.
*/
private static function get_block_nodes_private( $theme_json ) {
Copy link
Contributor Author

@scruffian scruffian Apr 21, 2022

Choose a reason for hiding this comment

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

This needs a better name!

This isn't new code, it's just extracted from get_style_nodes.

Copy link
Member

Choose a reason for hiding this comment

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

get_block_style_nodes ? 😄

$selectors = static::get_blocks_metadata();
$nodes = array();
if ( ! isset( $theme_json['styles'] ) ) {
return $nodes;
}

// Blocks.
if ( ! isset( $theme_json['styles']['blocks'] ) ) {
return $nodes;
}

foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
$selector = null;
if ( isset( $selectors[ $name ]['selector'] ) ) {
$selector = $selectors[ $name ]['selector'];
}

$duotone_selector = null;
if ( isset( $selectors[ $name ]['duotone'] ) ) {
$duotone_selector = $selectors[ $name ]['duotone'];
}

$nodes[] = array(
'name' => $name,
'path' => array( 'styles', 'blocks', $name ),
'selector' => $selector,
'duotone' => $duotone_selector,
);

if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
$nodes[] = array(
'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
'selector' => $selectors[ $name ]['elements'][ $element ],
);
}
}
}

return $nodes;
}

/**
* Gets the CSS rules for a particular block from theme.json.
*
* @param array $block_metadata Meta data about the block to get styles for.
*
* @return array Styles for the block.
*/
public function get_styles_for_block( $block_metadata ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a new function! It gets the Global Styles CSS for one block, which can then be added to that block's inline CSS.

$node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
$selector = $block_metadata['selector'];
$settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
$declarations = static::compute_style_properties( $node, $settings );
$block_rules = static::to_ruleset( $selector, $declarations );
return $block_rules;
}
}
101 changes: 101 additions & 0 deletions lib/compat/wordpress-6.1/get-global-styles-and-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/**
* API to interact with global settings & styles.
*
* @package gutenberg
*/

/**
* Returns the stylesheet resulting of merging core, theme, and user data.
* This is a duplicate of wp_get_global_stylesheet
*
* @since 5.9.0
*
* @param array $types Types of styles to load. Optional.
* It accepts 'variables', 'styles', 'presets' as values.
* If empty, it'll load all for themes with theme.json support
* and only [ 'variables', 'presets' ] for themes without theme.json support.
*
* @return string Stylesheet.
*/
function wp_get_global_stylesheet_gutenberg( $types = array() ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function is almost a copy of what we have in core

Copy link
Member

Choose a reason for hiding this comment

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

Is this any different to gutenberg_get_global_stylesheet() in lib/compat/wordpress-6.0/get-global-styles-and-settings.php ?

I think the general approach is to copy that function over to the new compat file and keep the gutenberg_ naming convention.

// Return cached value if it can be used and exists.
// It's cached by theme to make sure that theme switching clears the cache.
$can_use_cached = (
( empty( $types ) ) &&
( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) &&
( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) &&
( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) &&
! is_admin()
);
$transient_name = 'global_styles_' . get_stylesheet();
if ( $can_use_cached ) {
$cached = get_transient( $transient_name );
if ( $cached ) {
return $cached;
}
}

$tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This (and the line below) are the only differences - we need to call the _Gutenberg version of the class.


$supports_theme_json = WP_Theme_JSON_Resolver_Gutenberg::theme_has_support();
if ( empty( $types ) && ! $supports_theme_json ) {
$types = array( 'variables', 'presets' );
} elseif ( empty( $types ) ) {
$types = array( 'variables', 'styles', 'presets' );
}

/*
* If variables are part of the stylesheet,
* we add them for all origins (default, theme, user).
* This is so themes without a theme.json still work as before 5.9:
* they can override the default presets.
* See https://core.trac.wordpress.org/ticket/54782
*/
$styles_variables = '';
if ( in_array( 'variables', $types, true ) ) {
$styles_variables = $tree->get_stylesheet( array( 'variables' ) );
$types = array_diff( $types, array( 'variables' ) );
}

/*
* For the remaining types (presets, styles), we do consider origins:
*
* - themes without theme.json: only the classes for the presets defined by core
* - themes with theme.json: the presets and styles classes, both from core and the theme
*/
$styles_rest = '';
if ( ! empty( $types ) ) {
$origins = array( 'default', 'theme', 'custom' );
if ( ! $supports_theme_json ) {
$origins = array( 'default' );
}
$styles_rest = $tree->get_stylesheet( $types, $origins );
}

$stylesheet = $styles_variables . $styles_rest;

if ( $can_use_cached ) {
// Cache for a minute.
// This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites.
set_transient( $transient_name, $stylesheet, MINUTE_IN_SECONDS );
}

return $stylesheet;
}

/**
* Adds global style rules to the inline style for each block.
*/
function wp_add_global_styles_for_blocks() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is also a new function. It fetches the CSS for the block and adds it to the inline CSS block.

$tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
// TODO some nodes dont have a name...
$block_nodes = $tree->get_block_nodes();

foreach ( $block_nodes as $metadata ) {
$block_css = $tree->get_styles_for_block( $metadata );
$block_name = str_replace( 'core/', '', $metadata['name'] );
wp_add_inline_style( 'wp-block-' . $block_name, $block_css );
}

}
54 changes: 54 additions & 0 deletions lib/compat/wordpress-6.1/script-loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/**
* Load global styles assets in the front-end.
*
* @package gutenberg
*/

/**
* Enqueues the global styles defined via theme.json.
* This should replace wp_enqueue_global_styles.
*
* @since 5.8.0
*/
function gutenberg_enqueue_global_styles() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a copy of wp_enqueue_global_styles.

$separate_assets = wp_should_load_separate_core_block_assets();
$is_block_theme = wp_is_block_theme();
$is_classic_theme = ! $is_block_theme;

/*
* Global styles should be printed in the head when loading all styles combined.
* The footer should only be used to print global styles for classic themes with separate core assets enabled.
*
* See https://core.trac.wordpress.org/ticket/53494.
*/
if (
( $is_block_theme && doing_action( 'wp_footer' ) ) ||
( $is_classic_theme && doing_action( 'wp_footer' ) && ! $separate_assets ) ||
( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $separate_assets )
) {
return;
}

if ( $separate_assets ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only change - if we are loading separate block assets then we should include the Global Styles CSS in the block CSS rather than in the global-styles one.

// add each block as an inline css.
wp_add_global_styles_for_blocks();
}
$stylesheet = wp_get_global_stylesheet_gutenberg();

if ( empty( $stylesheet ) ) {
return;
}

wp_register_style( 'global-styles', false, array(), true, true );
wp_add_inline_style( 'global-styles', $stylesheet );
wp_enqueue_style( 'global-styles' );
}

remove_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles' );
remove_action( 'wp_footer', 'wp_enqueue_global_styles' );
remove_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_global_styles_assets' );
remove_action( 'wp_footer', 'gutenberg_enqueue_global_styles_assets' );

add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_global_styles' );
add_action( 'wp_footer', 'gutenberg_enqueue_global_styles', 1 );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to remove all the other times when we do this, so that we don't end up with many copies of the same styles.

19 changes: 19 additions & 0 deletions lib/experimental/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
/**
* WP_Theme_JSON_6_1 class
*
* @package gutenberg
*/

/**
* Class that encapsulates the processing of structures that adhere to the theme.json spec.
*
* 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 get_global_settings, get_global_styles, and get_global_stylesheet instead.
*
* @access private
*/
class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_6_1 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This enables us to always reference WP_Theme_JSON_Gutenberg in experimental libs, rather than targetting a specific version.


}
Loading