From ded6e2e4789931b3ced6dddfe397d7d18245feb3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 5 Feb 2026 22:48:01 -0800 Subject: [PATCH 01/22] Add failing test case to capture the Elementor scenario --- tests/phpunit/tests/template.php | 91 ++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index da18191b0983e..2060a080504cb 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -151,6 +151,7 @@ public function tear_down() { $registry->unregister( 'third-party/test' ); } + unset( $GLOBALS['_wp_tests_development_mode'] ); parent::tear_down(); } @@ -1480,6 +1481,8 @@ public function test_wp_load_classic_theme_block_styles_on_demand( string $theme * @return array */ public function data_wp_hoist_late_printed_styles(): array { + $blocks_content = '
This is only a test!
'; + $early_common_styles = array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', @@ -1487,12 +1490,14 @@ public function data_wp_hoist_late_printed_styles(): array { 'wp-emoji-styles-inline-css', ); - $common_late_in_head = array( - // Styles enqueued at wp_enqueue_scripts (priority 10). + // Styles enqueued at wp_enqueue_scripts (priority 10). + $common_at_wp_enqueue_scripts = array( 'normal-css', 'normal-inline-css', + ); - // Styles printed at wp_head priority 10. + $common_late_in_head = array( + // Styles printed at wp_head priority 100. 'wp-custom-css', ); @@ -1521,6 +1526,7 @@ public function data_wp_hoist_late_printed_styles(): array { // Hoisted. Enqueued by wp_enqueue_global_styles() which runs at wp_enqueue_scripts priority 10 and wp_footer priority 1. 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ); @@ -1528,6 +1534,7 @@ public function data_wp_hoist_late_printed_styles(): array { return array( 'standard_classic_theme_config_with_min_styles_inlined' => array( 'set_up' => null, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, @@ -1536,6 +1543,7 @@ public function data_wp_hoist_late_printed_styles(): array { ), 'standard_classic_theme_config_with_max_styles_inlined' => array( 'set_up' => null, + 'content' => $blocks_content, 'inline_size_limit' => PHP_INT_MAX, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1548,6 +1556,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'custom-block-styles-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ), @@ -1565,6 +1574,7 @@ static function () { 100 ); }, + 'content' => $blocks_content, 'inline_size_limit' => PHP_INT_MAX, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1576,6 +1586,7 @@ static function () { 'custom-block-styles-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ), @@ -1593,6 +1604,7 @@ static function () { 100 ); }, + 'content' => $blocks_content, 'inline_size_limit' => PHP_INT_MAX, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1603,6 +1615,7 @@ static function () { 'third-party-test-block-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ), @@ -1618,6 +1631,7 @@ static function ( $handles ) { } ); }, + 'content' => $blocks_content, 'inline_size_limit' => PHP_INT_MAX, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1629,6 +1643,7 @@ static function ( $handles ) { 'third-party-test-block-css', 'custom-block-styles-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ), @@ -1644,6 +1659,7 @@ static function () { } ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => ( function ( $expected_styles ) { @@ -1660,6 +1676,7 @@ static function () { 'set_up' => static function () { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1671,6 +1688,7 @@ static function () { 'custom-block-styles-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head ), 'BODY' => $common_late_in_body, @@ -1680,6 +1698,7 @@ static function () { 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, @@ -1690,6 +1709,7 @@ static function () { 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, @@ -1701,13 +1721,14 @@ static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), ), ), - 'disable_block_library' => array( + 'disable_block_library_and_load_combined' => array( 'set_up' => static function () { add_action( 'enqueue_block_assets', @@ -1718,6 +1739,7 @@ function (): void { ); add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1728,6 +1750,7 @@ function (): void { 'custom-block-styles-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, $common_late_in_head ), 'BODY' => $common_late_in_body, @@ -1743,6 +1766,7 @@ function (): void { } ); }, + 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( 'HEAD' => array_merge( @@ -1756,6 +1780,55 @@ function (): void { 'custom-block-styles-css', 'global-styles-inline-css', ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), + + // This tests the Elementor scenario. + 'dequeue_block_library_but_with_theme_json_and_no_block_content' => array( + 'set_up' => static function () { + add_action( + 'wp_enqueue_scripts', + static function () { + wp_dequeue_style( 'wp-block-library' ); + wp_dequeue_style( 'wp-block-library-theme' ); + wp_dequeue_style( 'custom-block-styles' ); + }, + 999 + ); + + /* + * Simulate the theme having a theme.json so that 'classic-theme-styles' is not enqueued. Note that + * when 'classic-theme-styles' is present, then 'global-styles' gets inserted after it by + * wp_hoist_late_printed_styles(). So by omitting 'classic-theme-styles', this can verify that + * 'global-styles' is still printed before other styles. + */ + $GLOBALS['_wp_tests_development_mode'] = 'theme'; + add_filter( + 'theme_file_path', + static function ( $path, $file ) { + if ( 'theme.json' === $file ) { + $path = __DIR__ . '/../data/themedir1/block-theme/theme.json'; + } + return $path; + }, + 10, + 2 + ); + }, + 'content' => 'Hello World!', + 'inline_size_limit' => 0, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, $common_late_in_head, $common_late_in_body ), @@ -1775,7 +1848,7 @@ function (): void { * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up, int $inline_size_limit, array $expected_styles ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, string $content, int $inline_size_limit, array $expected_styles ): void { // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); @@ -1867,11 +1940,7 @@ static function () { wp_add_inline_style( 'late', '/* LATE */' ); // Simulate the_content(). - $content = apply_filters( - 'the_content', - '
' . - '
This is only a test!
' - ); + $content = apply_filters( 'the_content', $content ); // Simulate footer scripts. $footer_output = get_echo( 'wp_footer' ); @@ -1880,7 +1949,7 @@ static function () { $buffer = '' . $head_output . '
' . $content . '
' . $footer_output . ''; $placeholder_regexp = '#/\*wp_block_styles_on_demand_placeholder:[a-f0-9]+\*/#'; - if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) { + if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) && wp_style_is( 'wp-block-library', 'enqueued' ) ) { $this->assertMatchesRegularExpression( $placeholder_regexp, $buffer, 'Expected the placeholder to be present in the buffer.' ); } From 485549b65c7cad5879365aacb0480db43a478813 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Feb 2026 11:59:31 -0800 Subject: [PATCH 02/22] Add global-styles placeholder which is replaced during hoisting --- src/wp-includes/script-loader.php | 62 +++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 87689423c37ac..f98cab8aa5228 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2546,21 +2546,42 @@ function wp_enqueue_global_styles() { $is_block_theme = wp_is_block_theme(); $is_classic_theme = ! $is_block_theme; - /* - * Global styles should be printed in the head for block themes, or for classic themes when loading assets on - * demand is disabled, which is the default. - * The footer should only be used for classic themes when loading assets on demand is enabled. + /** + * Global styles should be printed in the HEAD for block themes, or for classic themes when loading assets on + * demand is disabled (which is no longer the default). * - * See https://core.trac.wordpress.org/ticket/53494 and https://core.trac.wordpress.org/ticket/61965. + * @link https://core.trac.wordpress.org/ticket/53494 + * @link https://core.trac.wordpress.org/ticket/61965 */ if ( - ( $is_block_theme && doing_action( 'wp_footer' ) ) || - ( $is_classic_theme && doing_action( 'wp_footer' ) && ! $assets_on_demand ) || - ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $assets_on_demand ) + doing_action( 'wp_footer' ) && + ( + $is_block_theme || + ( $is_classic_theme && ! $assets_on_demand ) + ) ) { return; } + /** + * The footer should only be used for classic themes when loading assets on demand is enabled. This is now the + * default with the introduction of hoisting late-printed styles (via {@see wp_load_classic_theme_block_styles_on_demand()}). + * So even though the main global styles are not printed here in the HEAD for classic themes with on-demand asset + * loading, placeholder for the global styles is still enqueued. Then when {@see wp_hoist_late_printed_styles()} + * processes the output buffer, it can locate the placeholder and inject the global styles from the footer in to the + * HEAD. + * + * @link https://core.trac.wordpress.org/ticket/64099 + */ + if ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $assets_on_demand ) { + if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) { + wp_register_style( 'wp-global-styles-placeholder', false ); + wp_add_inline_style( 'wp-global-styles-placeholder', '/* Placeholder for wp_hoist_late_printed_styles() to replace with the global-styles printed at wp_footer. */' ); + wp_enqueue_style( 'wp-global-styles-placeholder' ); + } + return; + } + /* * If loading the CSS for each block separately, then load the theme.json CSS conditionally. * This removes the CSS from the global-styles stylesheet and adds it to the inline CSS for each block. @@ -3866,11 +3887,27 @@ public function remove() { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' ); } + + /** + * Replaces the current token. + * + * @param string $text Text to replace with. + */ + public function replace( string $text ) { + $span = $this->get_span(); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, $text ); + } }; // Locate the insertion points in the HEAD. while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) { if ( + 'STYLE' === $processor->get_tag() && + 'wp-global-styles-placeholder-inline-css' === $processor->get_attribute( 'id' ) + ) { + $processor->set_bookmark( 'wp_global_styles_placeholder' ); + } elseif ( 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { @@ -3902,6 +3939,15 @@ public function remove() { } } + /** + * Replace the placeholder for global styles enqueued during {@see wp_enqueue_global_styles()}. + */ + if ( '' !== $printed_global_styles && $processor->has_bookmark( 'wp_global_styles_placeholder' ) ) { + $processor->seek( 'wp_global_styles_placeholder' ); + $processor->replace( $printed_global_styles ); + $printed_global_styles = ''; + } + /* * Insert block styles right after wp-block-library (if it is present). The placeholder CSS comment will * always be added to the wp-block-library inline style since it gets printed at `wp_head` before the blocks From ec819adf1387018e76407170102076f47f61c605 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Feb 2026 12:09:59 -0800 Subject: [PATCH 03/22] Add test case for blockless theme without theme.json --- tests/phpunit/tests/template.php | 48 +++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 2060a080504cb..684a763b15007 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1788,7 +1788,7 @@ function (): void { ), ), - // This tests the Elementor scenario. + // This tests the Elementor scenario (e.g. Hello Elementor). 'dequeue_block_library_but_with_theme_json_and_no_block_content' => array( 'set_up' => static function () { add_action( @@ -1835,6 +1835,52 @@ static function ( $path, $file ) { 'BODY' => array(), ), ), + + // This tests the Elementor scenario but a theme.json is not present. + 'dequeue_block_library_but_without_theme_json_and_no_block_content' => array( + 'set_up' => static function () { + add_action( + 'wp_enqueue_scripts', + static function () { + wp_dequeue_style( 'wp-block-library' ); + wp_dequeue_style( 'wp-block-library-theme' ); + wp_dequeue_style( 'custom-block-styles' ); + }, + 999 + ); + + /* + * Simulate the theme NOT having a theme.json so that 'classic-theme-styles' is enqueued. + */ + $GLOBALS['_wp_tests_development_mode'] = 'theme'; + add_filter( + 'theme_file_path', + static function ( $path, $file ) { + if ( 'theme.json' === $file ) { + $path = __DIR__ . '/does-not-exist.json'; + } + return $path; + }, + 10, + 2 + ); + }, + 'content' => 'Hello World!', + 'inline_size_limit' => 0, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'classic-theme-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), ); } From 2f8713cd82f21ca7b3cca9ed92262b19d1a07f50 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Feb 2026 12:25:41 -0800 Subject: [PATCH 04/22] Ensure global styles placeholder is removed even when there is no global-styles --- src/wp-includes/script-loader.php | 5 +++-- tests/phpunit/tests/template.php | 26 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index f98cab8aa5228..70a0d991cc02a 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3940,9 +3940,10 @@ public function replace( string $text ) { } /** - * Replace the placeholder for global styles enqueued during {@see wp_enqueue_global_styles()}. + * Replace the placeholder for global styles enqueued during {@see wp_enqueue_global_styles()}. This is done + * even if $printed_global_styles is empty. */ - if ( '' !== $printed_global_styles && $processor->has_bookmark( 'wp_global_styles_placeholder' ) ) { + if ( $processor->has_bookmark( 'wp_global_styles_placeholder' ) ) { $processor->seek( 'wp_global_styles_placeholder' ); $processor->replace( $printed_global_styles ); $printed_global_styles = ''; diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 684a763b15007..fafeb1e62b969 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1624,12 +1624,11 @@ static function () { ), 'no_global_styles' => array( 'set_up' => static function () { - add_filter( - 'print_styles_array', - static function ( $handles ) { - return array_values( array_diff( $handles, array( 'global-styles' ) ) ); - } - ); + $dequeue = static function () { + wp_dequeue_style( 'global-styles' ); + }; + add_action( 'wp_enqueue_scripts', $dequeue, 1000 ); + add_action( 'wp_footer', $dequeue, 2 ); }, 'content' => $blocks_content, 'inline_size_limit' => PHP_INT_MAX, @@ -1994,9 +1993,15 @@ static function () { // Create a simulated output buffer. $buffer = '' . $head_output . '
' . $content . '
' . $footer_output . ''; - $placeholder_regexp = '#/\*wp_block_styles_on_demand_placeholder:[a-f0-9]+\*/#'; - if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) && wp_style_is( 'wp-block-library', 'enqueued' ) ) { - $this->assertMatchesRegularExpression( $placeholder_regexp, $buffer, 'Expected the placeholder to be present in the buffer.' ); + $global_styles_placeholder_regexp = '#\n"; + } + // If the classic-theme-styles is absent, then the third-party block styles cannot be inserted after it, so they get inserted here. if ( ! $processor->has_bookmark( 'classic_theme_styles' ) ) { if ( '' !== $printed_other_block_styles ) { diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 7b007f40fd889..178d9feba5cb4 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1478,7 +1478,13 @@ public function test_wp_load_classic_theme_block_styles_on_demand( string $theme /** * Data provider. * - * @return array + * @return array */ public function data_wp_hoist_late_printed_styles(): array { $blocks_content = '
This is only a test!
'; @@ -1541,6 +1547,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'BODY' => array(), ), ), + 'standard_classic_theme_config_with_max_styles_inlined' => array( 'set_up' => null, 'content' => $blocks_content, @@ -1563,6 +1570,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'BODY' => array(), ), ), + 'classic_theme_styles_omitted' => array( 'set_up' => static function () { // Note that wp_enqueue_scripts is used instead of enqueue_block_assets because it runs again at the former action. @@ -1593,6 +1601,7 @@ static function () { 'BODY' => array(), ), ), + 'no_styles_at_enqueued_block_assets' => array( 'set_up' => static function () { add_action( @@ -1622,6 +1631,7 @@ static function () { 'BODY' => array(), ), ), + 'no_global_styles' => array( 'set_up' => static function () { $dequeue = static function () { @@ -1649,28 +1659,89 @@ static function () { 'BODY' => array(), ), ), - 'standard_classic_theme_config_extra_block_library_inline_style' => array( + + 'standard_classic_theme_config_extra_block_library_inline_style_none_inlined' => array( 'set_up' => static function () { add_action( 'enqueue_block_assets', static function () { - wp_add_inline_style( 'wp-block-library', '/* Extra CSS which prevents empty inline style containing placeholder from being removed. */' ); + // Extra CSS which prevents empty inline style containing placeholder from being removed. + wp_add_inline_style( 'wp-block-library', '.wp-block-separator{ outline:solid 1px lime; }' ); } ); }, 'content' => $blocks_content, 'inline_size_limit' => 0, 'expected_styles' => array( - 'HEAD' => ( function ( $expected_styles ) { - // Insert 'wp-block-library-inline-css' right after 'wp-block-library-css'. - $i = array_search( 'wp-block-library-css', $expected_styles, true ); - $this->assertIsInt( $i, 'Expected wp-block-library-css to be among the styles.' ); - array_splice( $expected_styles, $i + 1, 0, 'wp-block-library-inline-css' ); - return $expected_styles; - } )( $common_expected_head_styles ), + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-css', + 'wp-block-separator-css', + 'wp-block-library-inline-css-extra', + 'classic-theme-styles-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + 'assert' => function ( string $buffer, string $filtered_buffer ) { + $block_separator_inline_style_start_tag = "assertStringContainsString( $block_separator_inline_style_start_tag, $filtered_buffer ); + $this->assertStringContainsString( $block_separator_custom_style, $filtered_buffer ); + $block_separator_inline_style_position = strpos( $filtered_buffer, $block_separator_inline_style_start_tag ); + $block_separator_custom_style_position = strpos( $filtered_buffer, $block_separator_custom_style ); + $this->assertTrue( $block_separator_custom_style_position > $block_separator_inline_style_position, 'Expected the block separator custom style to appear after the block separator stylesheet.' ); + }, + ), + + 'standard_classic_theme_config_extra_block_library_inline_style_all_inlined' => array( + 'set_up' => static function () { + add_action( + 'enqueue_block_assets', + static function () { + // Extra CSS which prevents empty inline style containing placeholder from being removed. + wp_add_inline_style( 'wp-block-library', '.wp-block-separator{ outline:solid 1px lime; }' ); + } + ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'wp-block-library-inline-css-extra', + 'classic-theme-styles-inline-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), 'BODY' => array(), ), + 'assert' => function ( string $buffer, string $filtered_buffer ) { + $block_separator_inline_style_start_tag = '\n"; + $style_processor = new WP_HTML_Tag_Processor( '' ); + $style_processor->next_tag(); + $style_processor->set_attribute( 'id', 'wp-block-library-inline-css-extra' ); + $style_processor->set_modifiable_text( $extra_inline_styles ); + $inserted_after .= "{$style_processor->get_updated_html()}\n"; } // If the classic-theme-styles is absent, then the third-party block styles cannot be inserted after it, so they get inserted here. From 80472f0807a25272c7ce3f5adc9ec0aa17ee6f61 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 21:23:40 -0800 Subject: [PATCH 13/22] Add conditional return types and remove redundant param type for has_action()/has_filter() --- src/wp-includes/plugin.php | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/plugin.php b/src/wp-includes/plugin.php index 0ca495b6f76d4..c1d68a81d713d 100644 --- a/src/wp-includes/plugin.php +++ b/src/wp-includes/plugin.php @@ -271,17 +271,23 @@ function apply_filters_ref_array( $hook_name, $args ) { * * @global WP_Hook[] $wp_filter Stores all of the filters and actions. * - * @param string $hook_name The name of the filter hook. - * @param callable|string|array|false $callback Optional. The callback to check for. - * This function can be called unconditionally to speculatively check - * a callback that may or may not exist. Default false. - * @param int|false $priority Optional. The specific priority at which to check for the callback. - * Default false. + * @param string $hook_name The name of the filter hook. + * @param callable|false $callback Optional. The callback to check for. + * This function can be called unconditionally to speculatively check + * a callback that may or may not exist. Default false. + * @param int|false $priority Optional. The specific priority at which to check for the callback. + * Default false. * @return bool|int If `$callback` is omitted, returns boolean for whether the hook has * anything registered. When checking a specific function, the priority * of that hook is returned, or false if the function is not attached. * If `$callback` and `$priority` are both provided, a boolean is returned * for whether the specific function is registered at that priority. + * + * @phpstan-return ( + * $callback is false ? bool : ( + * $priority is false ? int|false : bool + * ) + * ) */ function has_filter( $hook_name, $callback = false, $priority = false ) { global $wp_filter; @@ -583,17 +589,23 @@ function do_action_ref_array( $hook_name, $args ) { * * @see has_filter() This function is an alias of has_filter(). * - * @param string $hook_name The name of the action hook. - * @param callable|string|array|false $callback Optional. The callback to check for. - * This function can be called unconditionally to speculatively check - * a callback that may or may not exist. Default false. - * @param int|false $priority Optional. The specific priority at which to check for the callback. - * Default false. + * @param string $hook_name The name of the action hook. + * @param callable|false $callback Optional. The callback to check for. + * This function can be called unconditionally to speculatively check + * a callback that may or may not exist. Default false. + * @param int|false $priority Optional. The specific priority at which to check for the callback. + * Default false. * @return bool|int If `$callback` is omitted, returns boolean for whether the hook has * anything registered. When checking a specific function, the priority * of that hook is returned, or false if the function is not attached. * If `$callback` and `$priority` are both provided, a boolean is returned * for whether the specific function is registered at that priority. + * + * @phpstan-return ( + * $callback is false ? bool : ( + * $priority is false ? int|false : bool + * ) + * ) */ function has_action( $hook_name, $callback = false, $priority = false ) { return has_filter( $hook_name, $callback, $priority ); From 0c0f5291b939dc2d6d0ad3d528a006b9ed64eaa9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 21:25:42 -0800 Subject: [PATCH 14/22] Fix PHPStan level 8 issues in wp_hoist_late_printed_styles() --- src/wp-includes/script-loader.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index e65353ee4f6a9..b99350ae4b513 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3721,7 +3721,7 @@ function wp_load_classic_theme_block_styles_on_demand() { * @see wp_load_classic_theme_block_styles_on_demand() * @see _wp_footer_scripts() */ -function wp_hoist_late_printed_styles() { +function wp_hoist_late_printed_styles(): void { // Skip the embed template on-demand styles aren't relevant, and there is no wp_head action. if ( is_embed() ) { return; @@ -3798,7 +3798,7 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { if ( count( $enqueued_core_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_core_block_styles ); - $printed_core_block_styles = ob_get_clean(); + $printed_core_block_styles = (string) ob_get_clean(); } // Non-core block styles get printed after the classic-theme-styles stylesheet. @@ -3806,14 +3806,14 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { if ( count( $enqueued_other_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_other_block_styles ); - $printed_other_block_styles = ob_get_clean(); + $printed_other_block_styles = (string) ob_get_clean(); } // Capture the global-styles so that it can be printed separately after classic-theme-styles and other styles enqueued at enqueue_block_assets. if ( wp_style_is( 'global-styles' ) ) { ob_start(); wp_styles()->do_items( array( 'global-styles' ) ); - $printed_global_styles = ob_get_clean(); + $printed_global_styles = (string) ob_get_clean(); } /* @@ -3823,7 +3823,7 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { */ ob_start(); wp_styles()->do_footer_items(); - $printed_late_styles = ob_get_clean(); + $printed_late_styles = (string) ob_get_clean(); }; /* @@ -3874,7 +3874,7 @@ private function get_span(): WP_HTML_Span { * * @param string $text Text to insert. */ - public function insert_before( string $text ) { + public function insert_before( string $text ): void { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text ); } @@ -3883,7 +3883,7 @@ public function insert_before( string $text ) { * * @param string $text Text to insert. */ - public function insert_after( string $text ) { + public function insert_after( string $text ): void { $span = $this->get_span(); $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text ); @@ -3892,7 +3892,7 @@ public function insert_after( string $text ) { /** * Removes the current token. */ - public function remove() { + public function remove(): void { $span = $this->get_span(); $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' ); @@ -3903,7 +3903,7 @@ public function remove() { * * @param string $text Text to replace with. */ - public function replace( string $text ) { + public function replace( string $text ): void { $span = $this->get_span(); $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, $text ); @@ -3926,7 +3926,7 @@ public function replace( string $text ) { $processor->set_bookmark( 'head_end' ); break; } elseif ( ( 'STYLE' === $processor->get_tag() || 'LINK' === $processor->get_tag() ) && $processor->get_attribute( 'id' ) ) { - $id = $processor->get_attribute( 'id' ); + $id = (string) $processor->get_attribute( 'id' ); $handle = null; if ( 'STYLE' === $processor->get_tag() ) { if ( preg_match( '/^(.+)-inline-css$/', $id, $matches ) ) { From c82418bacb5ddaa8d4c7f9be72b8090d8e943e17 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 21:30:22 -0800 Subject: [PATCH 15/22] Address PHPStan issues with test_wp_hoist_late_printed_styles --- tests/phpunit/tests/template.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 777362ed3e8ed..8e228ebb74ba1 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1956,6 +1956,8 @@ static function ( $path, $file ) { * @covers ::wp_hoist_late_printed_styles * * @dataProvider data_wp_hoist_late_printed_styles + * + * @param string[] $expected_styles */ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, string $content, int $inline_size_limit, array $expected_styles, ?Closure $assert = null ): void { // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: @@ -2080,6 +2082,7 @@ static function () { 'BODY' => array(), ); $processor = WP_HTML_Processor::create_full_parser( $filtered_buffer ); + $this->assertInstanceOf( WP_HTML_Processor::class, $processor ); while ( $processor->next_tag() ) { $group = in_array( 'HEAD', $processor->get_breadcrumbs(), true ) ? 'HEAD' : 'BODY'; if ( From e72fc456f802c3535b688177b38e54e01c996b53 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 22:09:42 -0800 Subject: [PATCH 16/22] Clarify comment --- src/wp-includes/script-loader.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b99350ae4b513..f7eea93374b5f 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4010,9 +4010,10 @@ public function replace( string $text ): void { $printed_core_block_styles = ''; /* - * Add a new inline style for any user styles added wp_add_inline_style( 'wp-block-library', '...' ). + * Add a new inline style for any user styles added via wp_add_inline_style( 'wp-block-library', '...' ). * This must be added here after $printed_core_block_styles to preserve the original CSS cascade when - * the combined block library stylesheet was used. + * the combined block library stylesheet was used. The pattern here is checking to see if it is not just + * a sourceURL comment after the placeholder above is removed. */ if ( ! preg_match( ':^\s*/\*# sourceURL=\S+? \*/\s*$:s', $extra_inline_styles ) ) { $style_processor = new WP_HTML_Tag_Processor( '' ); From d54326f6f8ab94055c1419da4e858cecff40426b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 22:40:31 -0800 Subject: [PATCH 17/22] Eliminate looking for classic-theme-styles in favor of adding placeholder where block styles are enqueued --- src/wp-includes/script-loader.php | 40 +++++++++++++++++++------------ 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index f7eea93374b5f..f50e3018d0083 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2762,6 +2762,16 @@ function wp_should_load_block_assets_on_demand() { */ function wp_enqueue_registered_block_scripts_and_styles() { if ( wp_should_load_block_assets_on_demand() ) { + /** + * Add placeholder for where block styles would historically get enqueued in a classic theme when block assets + * are not loaded on demand. This happens right after {@see wp_common_block_scripts_and_styles()} is called + * at which time wp-block-library is enqueued. + */ + if ( ! wp_is_block_theme() && has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) { + wp_register_style( 'wp-block-styles-placeholder', false ); + wp_add_inline_style( 'wp-block-styles-placeholder', ':root { --wp-internal-comment: "Placeholder for wp_hoist_late_printed_styles() to replace with the block styles printed at wp_footer." }' ); + wp_enqueue_style( 'wp-block-styles-placeholder' ); + } return; } @@ -3916,11 +3926,19 @@ public function replace( string $text ): void { 'STYLE' === $processor->get_tag() && 'wp-global-styles-placeholder-inline-css' === $processor->get_attribute( 'id' ) ) { + /** This is added in {@see wp_enqueue_global_styles()} */ $processor->set_bookmark( 'wp_global_styles_placeholder' ); + } elseif ( + 'STYLE' === $processor->get_tag() && + 'wp-block-styles-placeholder-inline-css' === $processor->get_attribute( 'id' ) + ) { + /** This is added in {@see wp_enqueue_registered_block_scripts_and_styles()} */ + $processor->set_bookmark( 'wp_block_styles_placeholder' ); } elseif ( 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { + /** This is added here in {@see wp_hoist_late_printed_styles()} */ $processor->set_bookmark( 'wp_block_library' ); } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) { $processor->set_bookmark( 'head_end' ); @@ -3936,10 +3954,6 @@ public function replace( string $text ): void { $handle = $matches[1]; } - if ( 'classic-theme-styles' === $handle ) { - $processor->set_bookmark( 'classic_theme_styles' ); - } - if ( $handle && in_array( $handle, $style_handles_at_enqueue_block_assets, true ) ) { if ( ! $processor->has_bookmark( 'first_style_at_enqueue_block_assets' ) ) { $processor->set_bookmark( 'first_style_at_enqueue_block_assets' ); @@ -4023,23 +4037,19 @@ public function replace( string $text ): void { $inserted_after .= "{$style_processor->get_updated_html()}\n"; } - // If the classic-theme-styles is absent, then the third-party block styles cannot be inserted after it, so they get inserted here. - if ( ! $processor->has_bookmark( 'classic_theme_styles' ) ) { - if ( '' !== $printed_other_block_styles ) { - $inserted_after .= $printed_other_block_styles; - } - $printed_other_block_styles = ''; - } - if ( '' !== $inserted_after ) { $processor->insert_after( "\n" . $inserted_after ); } } // Insert third-party block styles right after the classic-theme-styles. - if ( '' !== $printed_other_block_styles && $processor->has_bookmark( 'classic_theme_styles' ) ) { - $processor->seek( 'classic_theme_styles' ); - $processor->insert_after( "\n" . $printed_other_block_styles ); + if ( $processor->has_bookmark( 'wp_block_styles_placeholder' ) ) { + $processor->seek( 'wp_block_styles_placeholder' ); + if ( '' !== $printed_other_block_styles ) { + $processor->replace( "\n" . $printed_other_block_styles ); + } else { + $processor->remove(); + } $printed_other_block_styles = ''; } From c09f349fc2235c8d1005e0b000c90dc5c242e854 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 22:54:09 -0800 Subject: [PATCH 18/22] Remove obsolete references to classic-theme-styles --- src/wp-includes/script-loader.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index f50e3018d0083..023c6e071ecaf 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3801,7 +3801,7 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { } /* - * First print all styles related to blocks which should be inserted right after the wp-block-library stylesheet + * First print all styles related to core blocks which should be inserted right after the wp-block-library stylesheet * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`. */ $enqueued_core_block_styles = array_values( array_intersect( $all_core_block_style_handles, wp_styles()->queue ) ); @@ -3811,7 +3811,7 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { $printed_core_block_styles = (string) ob_get_clean(); } - // Non-core block styles get printed after the classic-theme-styles stylesheet. + // Capture non-core block styles so they can get printed at the point where wp_enqueue_registered_block_scripts_and_styles() runs. $enqueued_other_block_styles = array_values( array_intersect( $all_other_block_style_handles, wp_styles()->queue ) ); if ( count( $enqueued_other_block_styles ) > 0 ) { ob_start(); @@ -3819,7 +3819,7 @@ static function () use ( &$style_handles_at_enqueue_block_assets ) { $printed_other_block_styles = (string) ob_get_clean(); } - // Capture the global-styles so that it can be printed separately after classic-theme-styles and other styles enqueued at enqueue_block_assets. + // Capture the global-styles so that it can be printed at the point where wp_enqueue_global_styles() runs. if ( wp_style_is( 'global-styles' ) ) { ob_start(); wp_styles()->do_items( array( 'global-styles' ) ); @@ -4042,7 +4042,7 @@ public function replace( string $text ): void { } } - // Insert third-party block styles right after the classic-theme-styles. + // Insert block styles at the point where wp_enqueue_registered_block_scripts_and_styles() normally enqueues styles. if ( $processor->has_bookmark( 'wp_block_styles_placeholder' ) ) { $processor->seek( 'wp_block_styles_placeholder' ); if ( '' !== $printed_other_block_styles ) { From 59bdc8d739a245dc731e73c6c6a60403efe3bd74 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 23:09:08 -0800 Subject: [PATCH 19/22] Fix grammatical typo in comment --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 023c6e071ecaf..5094a96abf017 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3988,7 +3988,7 @@ public function replace( string $text ): void { /* * Split the block library inline style by the placeholder to identify the original inlined CSS, which - * is likely would be common.css, followed by any inline styles which had been added by the theme or + * likely would be common.css, followed by any inline styles which had been added by the theme or * plugins via `wp_add_inline_style( 'wp-block-library', '...' )`. The separate block styles loaded on * demand will get inserted after the inlined common.css and before the extra inline styles added by the * user. From ddcb748d55f97af1a7f37c8fbc71dd605c8b515a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 23:12:13 -0800 Subject: [PATCH 20/22] Account for possibility of no sourceURL comment being present --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 5094a96abf017..464f22101d7e6 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4029,7 +4029,7 @@ public function replace( string $text ): void { * the combined block library stylesheet was used. The pattern here is checking to see if it is not just * a sourceURL comment after the placeholder above is removed. */ - if ( ! preg_match( ':^\s*/\*# sourceURL=\S+? \*/\s*$:s', $extra_inline_styles ) ) { + if ( ! preg_match( ':^\s*(/\*# sourceURL=\S+? \*/\s*)?$:s', $extra_inline_styles ) ) { $style_processor = new WP_HTML_Tag_Processor( '' ); $style_processor->next_tag(); $style_processor->set_attribute( 'id', 'wp-block-library-inline-css-extra' ); From 87a5ccba7d04b9cd92bf22ccee7967289f55fc12 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 00:51:07 -0800 Subject: [PATCH 21/22] Improve grammar in comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/script-loader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 464f22101d7e6..9ed8cde7e7321 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4011,8 +4011,8 @@ public function replace( string $text ): void { /* * The placeholder CSS comment was added to the inline style in order to force an inline STYLE tag to - * be printed. Now that the inline style has been located and the placeholder comment was be removed, if - * there is no CSS left in the STYLE tag after removal, then remove the STYLE entirely. + * be printed. Now that the inline style has been located and the placeholder comment has been removed, if + * there is no CSS left in the STYLE tag after removal, then remove the STYLE tag entirely. */ if ( '' === trim( $css_text ) ) { $processor->remove(); From 17443f419654d8265011c34d5b0c1f4583f4e672 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 00:55:05 -0800 Subject: [PATCH 22/22] Remove arg from being passed needlessly to assert closure Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/template.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 8e228ebb74ba1..8ab3f0547e301 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1483,7 +1483,7 @@ public function test_wp_load_classic_theme_block_styles_on_demand( string $theme * content: string, * inline_size_limit: int, * expected_styles: array{ HEAD: string[], BODY: string[] }, - * assert?: Closure( string, string, array{ HEAD: string[], BODY: string[] } ): void, + * assert?: Closure( string, string ): void, * }> */ public function data_wp_hoist_late_printed_styles(): array { @@ -2120,7 +2120,7 @@ static function () { ); if ( $assert ) { - $assert( $buffer, $filtered_buffer, $found_subset_styles ); + $assert( $buffer, $filtered_buffer ); } }