Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Duotone: lazily load settings
  • Loading branch information
sgomes committed Feb 23, 2026
commit 39d682982112c4ab470a7b1df7008a6ab4e1f913
8 changes: 4 additions & 4 deletions lib/block-supports/duotone.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
)
);

// Set up metadata prior to rendering any blocks.
// Remove the Core metadata setup (for older versions of Core).
// In newer versions, we don't need to set up metadata prior to rendering any blocks,
// since the computation gets deferred until it's needed.
if ( class_exists( 'WP_Duotone' ) ) {
remove_action( 'wp_loaded', array( 'WP_Duotone', 'set_global_styles_presets' ) );
remove_action( 'wp_loaded', array( 'WP_Duotone', 'set_global_style_block_names' ) );
}
Comment thread
sgomes marked this conversation as resolved.
Outdated
add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_styles_presets' ), 10 );
add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_style_block_names' ), 10 );

// Add classnames to blocks using duotone support.
if ( function_exists( 'wp_render_duotone_support' ) ) {
Expand All @@ -30,7 +30,7 @@
remove_filter( 'render_block', array( 'WP_Duotone', 'render_duotone_support' ) );
remove_filter( 'render_block_core/image', array( 'WP_Duotone', 'restore_image_outer_container' ) );
}
add_filter( 'render_block', array( 'WP_Duotone_Gutenberg', 'render_duotone_support' ), 10, 2 );
add_filter( 'render_block', array( 'WP_Duotone_Gutenberg', 'render_duotone_support' ), 10, 3 );
add_filter( 'render_block_core/image', array( 'WP_Duotone_Gutenberg', 'restore_image_outer_container' ), 10, 1 );

// Enqueue styles.
Expand Down
167 changes: 106 additions & 61 deletions lib/class-wp-duotone-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ class WP_Duotone_Gutenberg {
* …
* ]
*
* @var array
* @var null|array<string, string>
*/
private static $global_styles_block_names = array();
private static $global_styles_block_names;

/**
* An array of duotone filter data from global, theme, and custom presets.
Expand All @@ -68,9 +68,9 @@ class WP_Duotone_Gutenberg {
* …
* ]
*
* @var array
* @var null|array<string, array<string, string|string[]>>
*/
private static $global_styles_presets = array();
private static $global_styles_presets;

/**
* All of the duotone filter data from presets for CSS custom properties on
Expand Down Expand Up @@ -476,7 +476,7 @@ private static function is_preset( $duotone_attr ) {
$slug = self::get_slug_from_attribute( $duotone_attr );
$filter_id = self::get_filter_id( $slug );

return array_key_exists( $filter_id, self::$global_styles_presets );
return array_key_exists( $filter_id, self::get_all_global_styles_presets() );
}
Comment on lines 471 to 480

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

is_preset() always calls get_all_global_styles_presets(), which triggers gutenberg_get_global_settings() → WP_Theme_JSON_Resolver_Gutenberg::get_merged_data() even for non-preset string values like 'unset' (where get_slug_from_attribute() returns an empty string). Add an early return when the extracted slug is empty (and/or when the attribute doesn't match the preset var formats) to avoid forcing the expensive theme.json merge for simple CSS-string duotone values.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

is_preset() always calls get_all_global_styles_presets(), which triggers gutenberg_get_global_settings() → WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()

This is not true. get_all_global_styles_presets() only calls gutenberg_get_global_settings() the first time, and otherwise keeps a cache. That's kind of fundamental to understanding this PR.

If the concern is that is_preset() gets called too early for the first time, and thus triggers the expensive operations too early as well, then that is also not true, because it only gets called in render_duotone_support(), which is a callback for the render_block hook.

This comment is a shallow analysis of the code in question, and unrelated to what this PR is trying to do.


/**
Expand Down Expand Up @@ -646,39 +646,41 @@ private static function get_global_styles_presets( $sources ) {
/**
* Get the CSS selector for a block type.
*
* @param string $block_name The block name.
* @param WP_Block_Type $block_type Block type to check for support.
*
* @return ?string The CSS selector or null if there is no support.
* @return string|null The CSS selector or null if there is no support.
Comment thread
sgomes marked this conversation as resolved.
Outdated
*/
private static function get_selector( $block_name ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );

if ( $block_type && $block_type instanceof WP_Block_Type ) {
// Backwards compatibility with `supports.color.__experimentalDuotone`
// is provided via the `block_type_metadata_settings` filter. If
// `supports.filter.duotone` has not been set and the experimental
// property has been, the experimental property value is copied into
// `supports.filter.duotone`.
$duotone_support = $block_type->supports['filter']['duotone'] ?? false;
if ( ! $duotone_support ) {
return null;
}
private static function get_selector( $block_type ) {
if ( ! ( $block_type instanceof WP_Block_Type ) ) {
return null;
}

// If the experimental duotone support was set, that value is to be
// treated as a selector and requires scoping.
$experimental_duotone = $block_type->supports['color']['__experimentalDuotone'] ?? false;
if ( $experimental_duotone ) {
$root_selector = wp_get_block_css_selector( $block_type );
return is_string( $experimental_duotone )
? WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $experimental_duotone )
: $root_selector;
}
/*
* Backward compatibility with `supports.color.__experimentalDuotone`
* is provided via the `block_type_metadata_settings` filter. If
* `supports.filter.duotone` has not been set and the experimental
* property has been, the experimental property value is copied into
* `supports.filter.duotone`.
*/
$duotone_support = block_has_support( $block_type, array( 'filter', 'duotone' ) );
if ( ! $duotone_support ) {
return null;
}

// Regular filter.duotone support uses filter.duotone selectors with fallbacks.
return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true );
/*
* If the experimental duotone support was set, that value is to be
* treated as a selector and requires scoping.
*/
$experimental_duotone = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
if ( $experimental_duotone ) {
$root_selector = wp_get_block_css_selector( $block_type );
return is_string( $experimental_duotone )
? WP_Theme_JSON::scope_selector( $root_selector, $experimental_duotone )
: $root_selector;
}

return null;
// Regular filter.duotone support uses filter.duotone selectors with fallbacks.
return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true );
}

/**
Expand Down Expand Up @@ -731,7 +733,8 @@ private static function enqueue_custom_filter( $filter_id, $duotone_selector, $f
* @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'.
*/
private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) {
if ( ! array_key_exists( $filter_id, self::$global_styles_presets ) ) {
$global_styles_presets = self::get_all_global_styles_presets();
if ( ! array_key_exists( $filter_id, $global_styles_presets ) ) {
$error_message = sprintf(
/* translators: %s: duotone filter ID */
__( 'The duotone id "%s" is not registered in theme.json settings', 'gutenberg' ),
Expand All @@ -740,8 +743,8 @@ private static function enqueue_global_styles_preset( $filter_id, $duotone_selec
_doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
return;
}
self::$used_global_styles_presets[ $filter_id ] = self::$global_styles_presets[ $filter_id ];
self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, self::$global_styles_presets[ $filter_id ] );
self::$used_global_styles_presets[ $filter_id ] = $global_styles_presets[ $filter_id ];
self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $global_styles_presets[ $filter_id ] );
}

/**
Expand All @@ -752,14 +755,9 @@ private static function enqueue_global_styles_preset( $filter_id, $duotone_selec
* @param WP_Block_Type $block_type Block Type.
*/
public static function register_duotone_support( $block_type ) {
$has_duotone_support = false;
if ( $block_type instanceof WP_Block_Type ) {
// Previous `color.__experimentalDuotone` support flag is migrated
// to `filter.duotone` via `block_type_metadata_settings` filter.
$has_duotone_support = $block_type->supports['filter']['duotone'] ?? null;
}

if ( $has_duotone_support ) {
// Previous `color.__experimentalDuotone` support flag is migrated
// to `filter.duotone` via `block_type_metadata_settings` filter.
if ( block_has_support( $block_type, array( 'filter', 'duotone' ), null ) ) {
if ( ! $block_type->attributes ) {
$block_type->attributes = array();
}
Expand All @@ -777,32 +775,61 @@ public static function register_duotone_support( $block_type ) {
* We only want to process this one time. On block render we'll access and output only the needed presets for that page.
*
* @since 6.3.0
*
* @return array An array of global styles presets, keyed on the filter ID.
Comment thread
sgomes marked this conversation as resolved.
Outdated
*/
public static function set_global_styles_presets() {
private static function get_all_global_styles_presets() {
if ( isset( self::$global_styles_presets ) ) {
return self::$global_styles_presets;
}

// Get the per block settings from the theme.json.
$tree = gutenberg_get_global_settings();
$presets_by_origin = $tree['color']['duotone'] ?? array();
$presets_by_origin = _wp_array_get( $tree, array( 'color', 'duotone' ), array() );

self::$global_styles_presets = array();
foreach ( $presets_by_origin as $presets ) {
foreach ( $presets as $preset ) {
$filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) );

self::$global_styles_presets[ $filter_id ] = $preset;
}
}

return self::$global_styles_presets;
}

/**
* Ensure all possible duotone presets are ready.
* This function is deprecated, as it's no longer necessary to handle this manually.
* It is kept to ensure external users of this method don't break.
*
* @since 6.3.0
* @deprecated
*/
public static function set_global_styles_presets() {
self::get_all_global_styles_presets();
Comment thread
sgomes marked this conversation as resolved.
}

/**
* Scrape all block names from global styles and store in self::$global_styles_block_names
*
* @since 6.3.0
*
* @return null|array<string, string> An array of global style block slugs, keyed on the block name.
Comment thread
sgomes marked this conversation as resolved.
*/
public static function set_global_style_block_names() {
private static function get_all_global_style_block_names() {
if ( isset( self::$global_styles_block_names ) ) {
return self::$global_styles_block_names;
}

// Get the per block settings from the theme.json.
$tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
$block_nodes = $tree->get_styles_block_nodes();
$theme_json = $tree->get_raw_data();

self::$global_styles_block_names = array();

foreach ( $block_nodes as $block_node ) {
// This block definition doesn't include any duotone settings. Skip it.
if ( empty( $block_node['duotone'] ) ) {
Expand All @@ -828,32 +855,49 @@ public static function set_global_style_block_names() {
self::$global_styles_block_names[ $block_node['name'] ] = $slug;
}
}

return self::$global_styles_block_names;
}

/**
* Ensure all possible global style block names are ready.
* This function is deprecated, as it's no longer necessary to handle this manually.
* It is kept to ensure external users of this method don't break.
*
* @since 6.3.0
* @deprecated
*/
public static function set_global_style_block_names() {
self::get_all_global_style_block_names();
Comment thread
sgomes marked this conversation as resolved.
}

/**
* Render out the duotone CSS styles and SVG.
*
* @since 6.3.0
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @param WP_Block $wp_block The block instance.
* @return string Filtered block content.
*/
public static function render_duotone_support( $block_content, $block ) {
if ( null === $block['blockName'] ) {
public static function render_duotone_support( $block_content, $block, $wp_block ) {
if ( empty( $block_content ) || ! $block['blockName'] ) {
return $block_content;
}
$duotone_selector = self::get_selector( $wp_block->block_type );

Comment on lines +886 to +891

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

render_duotone_support() now requires a $wp_block argument and immediately dereferences $wp_block->block_type. This is a backward-compatibility break: the deprecated gutenberg_render_duotone_support() helper (lib/block-supports/duotone.php:429+) and any external callers still passing 2 args will fatal with “Too few arguments” / null deref. Make the third parameter optional (default null) and fall back to resolving the WP_Block_Type from $block['blockName'] when $wp_block is missing/invalid; also update the deprecated wrapper to pass the instance when available (or construct one).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is irrelevant, as the method is only used as a filter callback, and there are thus no backwards compatibility concerns.

if ( ! $duotone_selector ) {
return $block_content;
}

$duotone_selector = self::get_selector( $block['blockName'] );
$global_styles_block_names = self::get_all_global_style_block_names();

// The block should have a duotone attribute or have duotone defined in its theme.json to be processed.
$has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );
$has_global_styles_duotone = array_key_exists( $block['blockName'], self::$global_styles_block_names );
$has_global_styles_duotone = array_key_exists( $block['blockName'], $global_styles_block_names );

Comment on lines +896 to 901

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

render_duotone_support() calls get_all_global_style_block_names() (which runs WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()) before checking whether the block even needs global-styles duotone. For blocks that already have a style.color.duotone attribute, this extra theme.json merge is unnecessary and undermines the lazy-load goal. Compute $has_duotone_attribute first and only load global style block names when you actually need to evaluate/apply the global-styles fallback.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

get_all_global_style_block_names() only runs WP_Theme_JSON_Resolver_Gutenberg::get_merged_data() the first time, and then keeps a cache. This is a pretty fundamental part of this PR. get_merged_data will not be called for every block.

The proposed 'fix' would only help if every single block happened to have a style.color.duotone attribute, otherwise it would have to be calculated at least once. And given that there are plenty of blocks without duotone support, this would be a useless optimisation.

if (
! $duotone_selector ||
( ! $has_duotone_attribute && ! $has_global_styles_duotone )
) {
if ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) {
return $block_content;
}

Expand Down Expand Up @@ -898,7 +942,7 @@ public static function render_duotone_support( $block_content, $block ) {
self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data );
}
} elseif ( $has_global_styles_duotone ) {
$slug = self::$global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'.
$slug = $global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'.
$filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
$filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.

Expand Down Expand Up @@ -1017,14 +1061,15 @@ public static function output_footer_assets() {
* @return array The editor settings with duotone SVGs and CSS custom properties.
*/
public static function add_editor_settings( $settings ) {
if ( ! empty( self::$global_styles_presets ) ) {
$global_styles_presets = self::get_all_global_styles_presets();
if ( ! empty( $global_styles_presets ) ) {
if ( ! isset( $settings['styles'] ) ) {
$settings['styles'] = array();
}

$settings['styles'][] = array(
// For the editor we can add all of the presets by default.
'assets' => self::get_svg_definitions( self::$global_styles_presets ),
'assets' => self::get_svg_definitions( $global_styles_presets ),
// The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work.
'__unstableType' => 'svgs',
// These styles not generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings.
Expand All @@ -1033,7 +1078,7 @@ public static function add_editor_settings( $settings ) {

$settings['styles'][] = array(
// For the editor we can add all of the presets by default.
'css' => self::get_global_styles_presets( self::$global_styles_presets ),
'css' => self::get_global_styles_presets( $global_styles_presets ),
// This must be set and must be something other than 'theme' or they will be stripped out in the post editor <Editor> component.
'__unstableType' => 'presets',
// These styles are no longer generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings.
Expand Down
9 changes: 6 additions & 3 deletions phpunit/class-wp-duotone-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,32 @@ public function test_gutenberg_render_duotone_support_preset() {
'blockName' => 'core/image',
'attrs' => array( 'style' => array( 'color' => array( 'duotone' => 'var:preset|duotone|blue-orange' ) ) ),
);
$wp_block = new WP_Block( $block );
$block_content = '<figure class="wp-block-image size-full"><img src="/my-image.jpg" /></figure>';
$expected = '<figure class="wp-block-image size-full wp-duotone-blue-orange"><img src="/my-image.jpg" /></figure>';
$this->assertSame( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block ) );
$this->assertSame( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block, $wp_block ) );
}

public function test_gutenberg_render_duotone_support_css() {
$block = array(
'blockName' => 'core/image',
'attrs' => array( 'style' => array( 'color' => array( 'duotone' => 'unset' ) ) ),
);
$wp_block = new WP_Block( $block );
$block_content = '<figure class="wp-block-image size-full"><img src="/my-image.jpg" /></figure>';
$expected = '/<figure class="wp-block-image size-full wp-duotone-unset-\d+"><img src="\\/my-image.jpg" \\/><\\/figure>/';
$this->assertMatchesRegularExpression( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block ) );
$this->assertMatchesRegularExpression( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block, $wp_block ) );
}

public function test_gutenberg_render_duotone_support_custom() {
$block = array(
'blockName' => 'core/image',
'attrs' => array( 'style' => array( 'color' => array( 'duotone' => array( '#FFFFFF', '#000000' ) ) ) ),
);
$wp_block = new WP_Block( $block );
$block_content = '<figure class="wp-block-image size-full"><img src="/my-image.jpg" /></figure>';
$expected = '/<figure class="wp-block-image size-full wp-duotone-ffffff-000000-\d+"><img src="\\/my-image.jpg" \\/><\\/figure>/';
$this->assertMatchesRegularExpression( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block ) );
$this->assertMatchesRegularExpression( $expected, WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block, $wp_block ) );
}


Expand Down