From 137e732da1855277b0a007de24b48cf16b5d4e58 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 11:25:49 -0700 Subject: [PATCH 01/50] Use well-formed HTML in tests --- 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 306cba4c1e870..92ac045457fb2 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -610,7 +610,7 @@ public function test_wp_start_template_enhancement_output_buffer_begins_with_fil add_filter( 'wp_template_enhancement_output_buffer', static function () { - return 'Hey!'; + return 'Hey!'; } ); $level = ob_get_level(); @@ -1060,7 +1060,7 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { $footer_output = get_echo( 'wp_footer' ); // Create a simulated output buffer. - $buffer = '' . $head_output . '
Content
' . $footer_output . ''; + $buffer = '' . $head_output . '
Content
' . $footer_output . ''; // Apply the output buffer filter. $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer ); From f5d49cd81bba062432135247f295802d260ae48b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 11:26:45 -0700 Subject: [PATCH 02/50] Use HTTPS and fix late inline style contents --- tests/phpunit/tests/template.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 92ac045457fb2..de08f6fd0d612 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1037,8 +1037,8 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { switch_theme( 'default' ); - // Enqueue a style - wp_enqueue_style( 'early', 'http://example.com/style.css' ); + // Enqueue a style. + wp_enqueue_style( 'early', 'https://example.com/style.css' ); wp_add_inline_style( 'early', '/* EARLY */' ); wp_hoist_late_printed_styles(); @@ -1053,8 +1053,8 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { $this->assertStringContainsString( 'early', $head_output, 'Expected the early-enqueued stylesheet to be present.' ); // Enqueue a late style (after wp_head). - wp_enqueue_style( 'late', 'http://example.com/late-style.css', array(), null ); - wp_add_inline_style( 'late', '/* EARLY */' ); + wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null ); + wp_add_inline_style( 'late', '/* LATE */' ); // Simulate footer scripts. $footer_output = get_echo( 'wp_footer' ); From cccb1efabf5b23155a7d4fbaab4d2c492397e174 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 11:36:14 -0700 Subject: [PATCH 03/50] Add separate test case specifically for filtering print_late_styles --- tests/phpunit/tests/template.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index de08f6fd0d612..50b42eee197d0 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -998,6 +998,11 @@ public function data_wp_hoist_late_printed_styles(): array { 'no_actions_removed' => array( 'set_up' => null, ), + 'disabled_printing_late_styles' => array( + 'set_up' => static function () { + add_filter( 'print_late_styles', '__return_false', 1000 ); + }, + ), '_wp_footer_scripts_removed' => array( 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); @@ -1044,7 +1049,6 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { wp_hoist_late_printed_styles(); // Ensure late styles are printed. - add_filter( 'print_late_styles', '__return_false', 1000 ); $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' ); // Simulate wp_head. From 137b9def24f4337a9d25eeb0c3fb21cf6e8d0876 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 12:32:25 -0700 Subject: [PATCH 04/50] Add comment for PHPCompatibility issue --- 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 6430d2957453e..caecf3f8ec66a 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3685,7 +3685,7 @@ function ( $buffer ) use ( $placeholder, &$printed_late_styles ) { // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { public function get_span(): WP_HTML_Span { - $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class. + $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class. This can be removed once is released in v10. $instance->set_bookmark( 'here' ); return $instance->bookmarks['here']; } From 89752333e1260ac3a50aca877268b0ca9edfce44 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 21:25:24 -0700 Subject: [PATCH 05/50] Use more specific return type for WP_Block_Type_Registry::get_all_registered() --- src/wp-includes/class-wp-block-type-registry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block-type-registry.php b/src/wp-includes/class-wp-block-type-registry.php index 969f0f0f64a42..0751aa7c92902 100644 --- a/src/wp-includes/class-wp-block-type-registry.php +++ b/src/wp-includes/class-wp-block-type-registry.php @@ -150,7 +150,7 @@ public function get_registered( $name ) { * * @since 5.0.0 * - * @return WP_Block_Type[] Associative array of `$block_type_name => $block_type` pairs. + * @return array Associative array of `$block_type_name => $block_type` pairs. */ public function get_all_registered() { return $this->registered_block_types; From 6308437cbdf0f157e370281492b522c1ee4b1b10 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 21:25:54 -0700 Subject: [PATCH 06/50] Remove PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass suppression for PHPCS See https://github.com/WordPress/wordpress-develop/pull/10288#discussion_r2469686284 --- src/wp-includes/script-loader.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index caecf3f8ec66a..fef141fc702ec 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3685,9 +3685,8 @@ function ( $buffer ) use ( $placeholder, &$printed_late_styles ) { // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { public function get_span(): WP_HTML_Span { - $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class. This can be removed once is released in v10. - $instance->set_bookmark( 'here' ); - return $instance->bookmarks['here']; + $this->set_bookmark( 'here' ); + return $this->bookmarks['here']; } }; From 2d94ec63551d1acd7669b1c793e36eb48bf2fe4c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 28 Oct 2025 22:17:24 -0700 Subject: [PATCH 07/50] Add a rendered block to the output --- tests/phpunit/tests/template.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 50b42eee197d0..5c892157e03dd 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1060,11 +1060,16 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null ); wp_add_inline_style( 'late', '/* LATE */' ); + $content = apply_filters( + 'the_content', + '
' + ); + // Simulate footer scripts. $footer_output = get_echo( 'wp_footer' ); // Create a simulated output buffer. - $buffer = '' . $head_output . '
Content
' . $footer_output . ''; + $buffer = '' . $head_output . '
' . $content . '
' . $footer_output . ''; // Apply the output buffer filter. $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer ); From 2d537f7de36b23c47c6b96d251763aba05646602 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Oct 2025 16:53:22 -0700 Subject: [PATCH 08/50] Add failing test cases for test_wp_hoist_late_printed_styles --- tests/phpunit/tests/template.php | 157 +++++++++++++++++++++++++++---- 1 file changed, 138 insertions(+), 19 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 5c892157e03dd..9bf849cc256af 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -991,38 +991,111 @@ 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 { + $theme_supports = array( + 'wp-block-styles', + ); + + $expected_head_styles = array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-inline-css', + + 'wp-block-separator-inline-css', + 'global-styles-inline-css', + 'core-block-supports-inline-css', + 'classic-theme-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + + // TODO: This is unexpected. Just because something is enqueued late shouldn't necessitate that it be inserted right after wp-block-library and before a normal style enqueued at wp_enqueue_scripts. + 'late-css', + 'late-inline-css', + ); + return array( - 'no_actions_removed' => array( - 'set_up' => null, + // TODO: Add test case for embed template. + 'standard_classic_theme_config' => array( + 'set_up' => null, + 'theme_supports' => $theme_supports, + 'expected' => $expected_head_styles, + ), + 'wp_block_styles_not_supported' => array( + 'set_up' => null, + 'theme_supports' => array(), + // The following excludes 'wp-block-separator-inline-css' from $expected_head_styles. + 'expected' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-inline-css', + 'late-css', + 'late-inline-css', + + // TODO: The following three are enqueued in a different order compared to $expected_head_styles above. Why? + 'core-block-supports-inline-css', + 'classic-theme-styles-inline-css', + 'global-styles-inline-css', + + 'normal-css', + 'normal-inline-css', + ), ), 'disabled_printing_late_styles' => array( - 'set_up' => static function () { + 'set_up' => static function () { add_filter( 'print_late_styles', '__return_false', 1000 ); }, + 'theme_supports' => $theme_supports, + 'expected' => $expected_head_styles, ), '_wp_footer_scripts_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, + 'theme_supports' => $theme_supports, + 'expected' => $expected_head_styles, ), 'wp_print_footer_scripts_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, + 'theme_supports' => $theme_supports, + 'expected' => $expected_head_styles, ), 'both_actions_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, + 'theme_supports' => $theme_supports, + 'expected' => $expected_head_styles, ), 'block_library_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { wp_deregister_style( 'wp-block-library' ); }, + 'theme_supports' => array(), + 'expected' => array_values( + array_diff( + $expected_head_styles, + array( + 'wp-block-separator-inline-css', + 'wp-block-library-css', + 'wp-block-library-inline-css', + 'core-block-supports-inline-css', + 'classic-theme-styles-inline-css', + 'global-styles-inline-css', + ) + ) + ), ), ); } @@ -1031,21 +1104,52 @@ public function data_wp_hoist_late_printed_styles(): array { * Tests that wp_hoist_late_printed_styles() adds a placeholder for delayed CSS, then removes it and adds all CSS to the head including late enqueued styles. * * @ticket 64099 + * @covers ::wp_load_classic_theme_block_styles_on_demand * @covers ::wp_hoist_late_printed_styles * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected ): void { + switch_theme( 'default' ); + + foreach ( $theme_supports as $theme_support ) { + add_theme_support( $theme_support ); + } + + add_filter( + 'wp_get_custom_css', + static function () { + return '/* CUSTOM CSS from Customizer */'; + } + ); + if ( $set_up ) { $set_up(); } - switch_theme( 'default' ); + wp_load_classic_theme_block_styles_on_demand(); + + $block_registry = WP_Block_Type_Registry::get_instance(); + foreach ( array_keys( $block_registry->get_all_registered() ) as $block_name ) { + $block_registry->unregister( $block_name ); + } + register_core_block_types_from_metadata(); - // Enqueue a style. + $this->assertFalse( wp_is_block_theme(), 'Test is only relevant to block themes.' ); + + // Enqueue a style early, before wp_enqueue_scripts. wp_enqueue_style( 'early', 'https://example.com/style.css' ); wp_add_inline_style( 'early', '/* EARLY */' ); + // Enqueue a style at the normal spot. + add_action( + 'wp_enqueue_scripts', + static function () { + wp_enqueue_style( 'normal', 'https://example.com/normal.css' ); + wp_add_inline_style( 'normal', '/* NORMAL */' ); + } + ); + wp_hoist_late_printed_styles(); // Ensure late styles are printed. @@ -1094,19 +1198,13 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void { } } - $expected = array( - 'early-css', - 'early-inline-css', - 'late-css', - 'late-inline-css', - ); foreach ( $expected as $style_id ) { - $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD.' ); + $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD among this snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) ); } $this->assertSame( $expected, array_values( array_intersect( $found_styles['HEAD'], $expected ) ), - 'Expected styles to be printed in the same order.' + 'Expected styles to be printed in the same order. Snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) ); $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' ); } @@ -1118,6 +1216,27 @@ public function assertTemplateHierarchy( $url, array $expected, $message = '' ) $this->assertSame( $expected, $hierarchy, $message ); } + /** + * Export PHP array as string formatted for pasting into unit test case. + * + * @param array $snapshot Snapshot. + * @return string Snapshot export. + */ + protected static function get_array_snapshot_export( array $snapshot ): string { + $export = var_export( $snapshot, true ); + $export = preg_replace( '/\barray \($/m', 'array(', $export ); + if ( isset( $snapshot[0] ) ) { + $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export ); + } + return preg_replace_callback( + '/(^ +)/m', + static function ( $matches ) { + return str_repeat( "\t", strlen( $matches[0] ) / 2 ); + }, + $export + ); + } + protected static function get_query_template_conditions() { return array( 'embed' => 'is_embed', From 92cd33caf5920158aaa89302e6dc6aa20e71ffd3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Oct 2025 17:10:19 -0700 Subject: [PATCH 09/50] Exclude PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass in script-loader.php --- phpcompat.xml.dist | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/phpcompat.xml.dist b/phpcompat.xml.dist index 66ea48ab78b8e..daf3cdbe52c93 100644 --- a/phpcompat.xml.dist +++ b/phpcompat.xml.dist @@ -114,4 +114,12 @@ /sodium_compat/src/PHP52/SplFixedArray\.php$ + + + /src/wp-includes/script-loader\.php$ + + From 53d21e632608673d22117077f52ad0eab53d2e4d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Nov 2025 00:44:50 -0700 Subject: [PATCH 10/50] Print block styles after wp-block-library and everything else at end of HEAD --- src/wp-includes/script-loader.php | 100 +++++++++++++++++++++++------- tests/phpunit/tests/template.php | 15 +++-- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index fef141fc702ec..eaf378e1130ed 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3634,7 +3634,7 @@ function wp_hoist_late_printed_styles() { * enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can * always hoist it to the HEAD. */ - add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX ); + add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX ); // TODO: Remove. /* * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header @@ -3645,11 +3645,44 @@ function wp_hoist_late_printed_styles() { wp_add_inline_style( 'wp-block-library', $placeholder ); // Wrap print_late_styles() with a closure that captures the late-printed styles. - $printed_late_styles = ''; - $capture_late_styles = static function () use ( &$printed_late_styles ) { + $printed_block_styles = ''; + $printed_late_styles = ''; + $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) { + global $concatenate_scripts; + script_concat_settings(); + + // Print the styles related to on-demand block enqueues. + $all_block_style_handles = array(); + foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { + foreach ( $block_type->style_handles as $style_handle ) { + $all_block_style_handles[] = $style_handle; + } + } + $all_block_style_handles = array_merge( + $all_block_style_handles, + array( 'global-styles', 'core-block-supports', 'block-style-variation-styles' ) // TODO: What else? + ); + + $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) ); + if ( count( $enqueued_block_styles ) > 0 ) { + wp_styles()->do_concat = $concatenate_scripts; + ob_start(); + wp_styles()->do_items( $enqueued_block_styles ); + _print_styles(); + $printed_block_styles = ob_get_clean(); + wp_styles()->reset(); + } + + /* + * Print remaining styles not related to blocks. This is the same logic as in print_late_styles(), but without + * the filter to control whether late styles are printed (since they are being hoisted anyway). + */ + wp_styles()->do_concat = $concatenate_scripts; ob_start(); - print_late_styles(); + wp_styles()->do_footer_items(); + _print_styles(); $printed_late_styles = ob_get_clean(); + wp_styles()->reset(); }; /* @@ -3680,7 +3713,7 @@ static function () use ( $capture_late_styles ) { // Replace placeholder with the captured late styles. add_filter( 'wp_template_enhancement_output_buffer', - function ( $buffer ) use ( $placeholder, &$printed_late_styles ) { + function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) { // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { @@ -3690,19 +3723,18 @@ public function get_span(): WP_HTML_Span { } }; - // Loop over STYLE tags. - while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - - // We've encountered the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment. + // TODO: If there are no block styles to print, it would be nice to not have to replace the placehoolder comment. + // Locate the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment. + while ( $processor->next_tag( array( 'tag_name' => 'STYLE' ) ) ) { if ( ! $processor->is_tag_closer() && 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { - // If the inline style lacks the placeholder comment, then we have to continue until we get to to append the styles there. + // If the inline style lacks the placeholder comment, then all the styles will be inserted below before . $css_text = $processor->get_modifiable_text(); if ( ! str_contains( $css_text, $placeholder ) ) { - continue; + break; } // Remove the placeholder now that we've located the inline style. @@ -3715,25 +3747,47 @@ public function get_span(): WP_HTML_Span { '', array( substr( $buffer, 0, $span->start + $span->length ), - $printed_late_styles, + $printed_block_styles, substr( $buffer, $span->start + $span->length ), ) ); + + // Prevent printing them again. + $printed_block_styles = ''; break; } + } + + // If there are remaining styles to hoist, either because + if ( $printed_block_styles || $printed_late_styles ) { + + // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. + $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { + public function get_span(): WP_HTML_Span { + $this->set_bookmark( 'here' ); + return $this->bookmarks['here']; + } + }; // As a fallback, append the hoisted late styles to the end of the HEAD. - if ( $processor->is_tag_closer() && 'HEAD' === $processor->get_tag() ) { - $span = $processor->get_span(); - $buffer = implode( - '', - array( - substr( $buffer, 0, $span->start ), - $printed_late_styles, - substr( $buffer, $span->start ), - ) - ); - break; + while ( $processor->next_tag( + array( + 'tag_name' => 'HEAD', + 'tag_closers' => 'visit', + ) + ) ) { + if ( $processor->is_tag_closer() ) { + $span = $processor->get_span(); + $buffer = implode( + '', + array( + substr( $buffer, 0, $span->start ), + $printed_block_styles . $printed_late_styles, + substr( $buffer, $span->start ), + ) + ); + break; + } } } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index ac1024192c192..a8eb5801d9143 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1075,9 +1075,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', - 'wp-block-library-css', 'wp-block-library-inline-css', - 'wp-block-separator-inline-css', 'global-styles-inline-css', 'core-block-supports-inline-css', @@ -1109,16 +1107,14 @@ public function data_wp_hoist_late_printed_styles(): array { 'wp-emoji-styles-inline-css', 'wp-block-library-css', 'wp-block-library-inline-css', - 'late-css', - 'late-inline-css', - - // TODO: The following three are enqueued in a different order compared to $expected_head_styles above. Why? 'core-block-supports-inline-css', 'classic-theme-styles-inline-css', 'global-styles-inline-css', - 'normal-css', 'normal-inline-css', + 'wp-custom-css', + 'late-css', + 'late-inline-css', ), ), 'disabled_printing_late_styles' => array( @@ -1183,6 +1179,8 @@ public function data_wp_hoist_late_printed_styles(): array { */ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected ): void { switch_theme( 'default' ); + global $wp_styles; + $wp_styles = null; foreach ( $theme_supports as $theme_support ) { add_theme_support( $theme_support ); @@ -1205,6 +1203,7 @@ static function () { foreach ( array_keys( $block_registry->get_all_registered() ) as $block_name ) { $block_registry->unregister( $block_name ); } + register_core_block_style_handles(); register_core_block_types_from_metadata(); $this->assertFalse( wp_is_block_theme(), 'Test is only relevant to block themes.' ); @@ -1252,7 +1251,7 @@ static function () { $this->assertStringContainsString( '', $buffer, 'Expected the closing HEAD tag to be in the response.' ); - $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9-]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' ); + $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' ); $found_styles = array( 'HEAD' => array(), 'BODY' => array(), From 156c6f5cdd991781a0f0593869e957156e91fcb0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 20:17:51 -0800 Subject: [PATCH 11/50] Add example for get_array_snapshot_export Co-authored-by: Dennis Snell --- tests/phpunit/tests/template.php | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index ce5d99ea9ac0b..fb6f9ee9c3100 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1700,15 +1700,38 @@ public function assertTemplateHierarchy( $url, array $expected, $message = '' ) } /** - * Export PHP array as string formatted for pasting into unit test case. + * Exports PHP array as string formatted as a snapshot for pasting into a data provider. + * + * Unfortunately, `var_export()` always includes array indices even for lists. For example: + * + * var_export( array( 'a', 'b', 'c' ) ); + * + * Results in: + * + * array ( + * 0 => 'a', + * 1 => 'b', + * 2 => 'c', + * ) + * + * This makes it unhelpful when outputting a snapshot to update a unit test. So this function strips out the indices + * to facilitate copy/pasting the snapshot from an assertion error message into the data provider. For example: + * + * array( + * 'a', + * 'b', + * 'c', + * ) + * + * This does not currently support nested arrays. * * @param array $snapshot Snapshot. * @return string Snapshot export. */ - protected static function get_array_snapshot_export( array $snapshot ): string { + private static function get_array_snapshot_export( array $snapshot ): string { $export = var_export( $snapshot, true ); $export = preg_replace( '/\barray \($/m', 'array(', $export ); - if ( isset( $snapshot[0] ) ) { + if ( array_is_list( $snapshot ) ) { $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export ); } return preg_replace_callback( From 16d3f544c2ea8f31d38ef5fd1fda78d4eb26dd01 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 21:21:07 -0800 Subject: [PATCH 12/50] Prevent enabling should_load_block_assets_on_demand if should_load_separate_core_block_assets was disabled --- src/wp-includes/script-loader.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index cee7ca7c973f9..b52e4baa31fb4 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3607,20 +3607,23 @@ function wp_load_classic_theme_block_styles_on_demand() { // The following two filters are added by default for block themes in _add_default_theme_supports(). /* - * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, - * and so that block-specific styles will only be enqueued when they are used on the page. - * A priority of zero allows for this to be easily overridden by themes which wish to opt out. + * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, and so + * that block-specific styles will only be enqueued when they are used on the page. A priority of zero allows for + * this to be easily overridden by themes which wish to opt out. If a site has explicitly opted out of loading + * separate block styles, then abort. */ add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 ); + if ( ! wp_should_load_separate_core_block_assets() ) { + return; + } /* * Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets). - * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. + * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. If a site + * has explicitly opted out of loading block styles on demand, then abort. */ add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 ); - - // If a site has explicitly opted out of loading block styles on demand via filters with priorities higher than above, then abort. - if ( ! wp_should_load_separate_core_block_assets() || ! wp_should_load_block_assets_on_demand() ) { + if ( ! wp_should_load_block_assets_on_demand() ) { return; } From 99b1b42d6c3ded37478ea4b183d01677acc0685c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 21:21:32 -0800 Subject: [PATCH 13/50] Improve tests --- tests/phpunit/tests/template.php | 144 +++++++++++++++++++------------ 1 file changed, 88 insertions(+), 56 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index fb6f9ee9c3100..63cd46e797ef0 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1475,7 +1475,7 @@ 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 { $theme_supports = array( @@ -1489,93 +1489,121 @@ public function data_wp_hoist_late_printed_styles(): array { 'wp-emoji-styles-inline-css', 'wp-block-library-inline-css', 'wp-block-separator-inline-css', + 'wp-block-separator-theme-inline-css', 'global-styles-inline-css', 'core-block-supports-inline-css', 'classic-theme-styles-inline-css', 'normal-css', 'normal-inline-css', 'wp-custom-css', - - // TODO: This is unexpected. Just because something is enqueued late shouldn't necessitate that it be inserted right after wp-block-library and before a normal style enqueued at wp_enqueue_scripts. 'late-css', 'late-inline-css', ); return array( // TODO: Add test case for embed template. - 'standard_classic_theme_config' => array( - 'set_up' => null, - 'theme_supports' => $theme_supports, - 'expected' => $expected_head_styles, + 'standard_classic_theme_config' => array( + 'set_up' => null, + 'theme_supports' => $theme_supports, + 'expected_head' => $expected_head_styles, + 'expected_footer' => array(), ), - 'wp_block_styles_not_supported' => array( - 'set_up' => null, - 'theme_supports' => array(), - // The following excludes 'wp-block-separator-inline-css' from $expected_head_styles. - 'expected' => array( + 'classic_theme_opt_out_separate_block_styles' => array( + 'set_up' => static function () { + add_filter( 'should_load_separate_core_block_assets', '__return_false' ); + }, + 'theme_supports' => $theme_supports, + 'expected_head' => array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', 'wp-block-library-css', - 'wp-block-library-inline-css', - 'core-block-supports-inline-css', + 'wp-block-library-theme-inline-css', 'classic-theme-styles-inline-css', 'global-styles-inline-css', 'normal-css', 'normal-inline-css', 'wp-custom-css', + ), + 'expected_footer' => array( 'late-css', 'late-inline-css', ), ), - 'disabled_printing_late_styles' => array( - 'set_up' => static function () { + 'wp_block_styles_not_supported' => array( + 'set_up' => null, + 'theme_supports' => array(), + 'expected_head' => array_values( + array_diff( + $expected_head_styles, + array( + 'wp-block-separator-theme-inline-css', + ) + ) + ), + 'expected_footer' => array(), + ), + 'disabled_printing_late_styles' => array( + 'set_up' => static function () { add_filter( 'print_late_styles', '__return_false', 1000 ); }, - 'theme_supports' => $theme_supports, - 'expected' => $expected_head_styles, + 'theme_supports' => $theme_supports, + 'expected_head' => $expected_head_styles, + 'expected_footer' => array(), ), - '_wp_footer_scripts_removed' => array( - 'set_up' => static function () { + '_wp_footer_scripts_removed' => array( + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, - 'theme_supports' => $theme_supports, - 'expected' => $expected_head_styles, + 'theme_supports' => $theme_supports, + 'expected_head' => $expected_head_styles, + 'expected_footer' => array(), ), - 'wp_print_footer_scripts_removed' => array( - 'set_up' => static function () { + 'wp_print_footer_scripts_removed' => array( + 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, - 'theme_supports' => $theme_supports, - 'expected' => $expected_head_styles, + 'theme_supports' => $theme_supports, + 'expected_head' => $expected_head_styles, + 'expected_footer' => array(), ), - 'both_actions_removed' => array( - 'set_up' => static function () { + 'both_actions_removed' => array( + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, - 'theme_supports' => $theme_supports, - 'expected' => $expected_head_styles, + 'theme_supports' => $theme_supports, + 'expected_head' => $expected_head_styles, + 'expected_footer' => array(), ), - 'block_library_removed' => array( - 'set_up' => static function () { - wp_deregister_style( 'wp-block-library' ); + 'disable_block_library' => array( + 'set_up' => static function () { + add_action( + 'enqueue_block_assets', + function (): void { + wp_deregister_style( 'wp-block-library' ); + wp_register_style( 'wp-block-library', '' ); + } + ); + add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, - 'theme_supports' => array(), - 'expected' => array_values( - array_diff( - $expected_head_styles, - array( - 'wp-block-separator-inline-css', - 'wp-block-library-css', - 'wp-block-library-inline-css', - 'core-block-supports-inline-css', - 'classic-theme-styles-inline-css', - 'global-styles-inline-css', - ) - ) + 'theme_supports' => array(), + 'expected_head' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'classic-theme-styles-inline-css', + 'global-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + 'core-block-supports-inline-css', + 'late-css', + 'late-inline-css', ), + 'expected_footer' => array(), ), ); } @@ -1589,7 +1617,7 @@ public function data_wp_hoist_late_printed_styles(): array { * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected_head, array $expected_footer ): void { switch_theme( 'default' ); global $wp_styles; $wp_styles = null; @@ -1616,7 +1644,7 @@ static function () { $block_registry->unregister( $block_name ); } register_core_block_style_handles(); - register_core_block_types_from_metadata(); + register_core_block_types_from_metadata(); // See register_block_type_from_metadata(). $this->assertFalse( wp_is_block_theme(), 'Test is only relevant to block themes.' ); @@ -1633,7 +1661,10 @@ static function () { } ); - wp_hoist_late_printed_styles(); + // Call wp_hoist_late_printed_styles() if wp_load_classic_theme_block_styles_on_demand() queued it up. + if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) { + wp_hoist_late_printed_styles(); + } // Ensure late styles are printed. $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' ); @@ -1681,15 +1712,16 @@ static function () { } } - foreach ( $expected as $style_id ) { - $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD among this snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) ); - } $this->assertSame( - $expected, - array_values( array_intersect( $found_styles['HEAD'], $expected ) ), - 'Expected styles to be printed in the same order. Snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) + $expected_head, + $found_styles['HEAD'], + 'Expected the same styles in the HEAD. Snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) + ); + $this->assertSame( + $expected_head, + $found_styles['HEAD'], + 'Expected the same styles in the BODY. Snapshot: ' . self::get_array_snapshot_export( $found_styles['BODY'] ) ); - $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' ); } public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { From 28faf6db84b273e7e89125be3576d7364ea18125 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 21:24:31 -0800 Subject: [PATCH 14/50] Remove obsolete print_late_styles override --- src/wp-includes/script-loader.php | 11 +---------- tests/phpunit/tests/template.php | 3 --- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b52e4baa31fb4..d86c2340c2ecb 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3645,15 +3645,6 @@ function wp_hoist_late_printed_styles() { return; } - /* - * While normally late styles are printed, there is a filter to disable prevent this, so this makes sure they are - * printed. Note that this filter was intended to control whether to print the styles queued too late for the HTML - * head. This filter was introduced in . However, with the template - * enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can - * always hoist it to the HEAD. - */ - add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX ); // TODO: Remove. - /* * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header * by means of a filter below on the template enhancement output buffer. @@ -3741,7 +3732,7 @@ public function get_span(): WP_HTML_Span { } }; - // TODO: If there are no block styles to print, it would be nice to not have to replace the placehoolder comment. + // TODO: If there are no block styles to print, it would be nice to not have to replace the placeholder comment. // Locate the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment. while ( $processor->next_tag( array( 'tag_name' => 'STYLE' ) ) ) { if ( diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 63cd46e797ef0..ecb6bab34c5e0 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1666,9 +1666,6 @@ static function () { wp_hoist_late_printed_styles(); } - // Ensure late styles are printed. - $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' ); - // Simulate wp_head. $head_output = get_echo( 'wp_head' ); From d3058045ecc450549e61ff40b96a0b1eaff434b1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 21:25:41 -0800 Subject: [PATCH 15/50] Update tests --- tests/phpunit/tests/template.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index ecb6bab34c5e0..1434cdec86786 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1531,6 +1531,27 @@ public function data_wp_hoist_late_printed_styles(): array { 'late-inline-css', ), ), + 'classic_theme_opt_out_separate_block_styles_and_no_footer_printing' => array( + 'set_up' => static function () { + add_filter( 'should_load_separate_core_block_assets', '__return_false' ); + add_filter( 'print_late_styles', '__return_false' ); + }, + 'theme_supports' => $theme_supports, + 'expected_head' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-theme-inline-css', + 'classic-theme-styles-inline-css', + 'global-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + ), + 'expected_footer' => array(), + ), 'wp_block_styles_not_supported' => array( 'set_up' => null, 'theme_supports' => array(), @@ -1599,11 +1620,12 @@ function (): void { 'normal-css', 'normal-inline-css', 'wp-custom-css', + ), + 'expected_footer' => array( 'core-block-supports-inline-css', 'late-css', 'late-inline-css', ), - 'expected_footer' => array(), ), ); } @@ -1646,7 +1668,7 @@ static function () { register_core_block_style_handles(); register_core_block_types_from_metadata(); // See register_block_type_from_metadata(). - $this->assertFalse( wp_is_block_theme(), 'Test is only relevant to block themes.' ); + $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); // Enqueue a style early, before wp_enqueue_scripts. wp_enqueue_style( 'early', 'https://example.com/style.css' ); From e8482a0cc9a433fb75485eac71c8a4a0803df37b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 22:55:35 -0800 Subject: [PATCH 16/50] Add todo comments --- 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 d86c2340c2ecb..052259eeff6ca 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3677,7 +3677,7 @@ function wp_hoist_late_printed_styles() { wp_styles()->do_concat = $concatenate_scripts; ob_start(); wp_styles()->do_items( $enqueued_block_styles ); - _print_styles(); + _print_styles(); // TODO: Is this needed? $printed_block_styles = ob_get_clean(); wp_styles()->reset(); } @@ -3689,7 +3689,7 @@ function wp_hoist_late_printed_styles() { wp_styles()->do_concat = $concatenate_scripts; ob_start(); wp_styles()->do_footer_items(); - _print_styles(); + _print_styles(); // TODO: Is this needed? $printed_late_styles = ob_get_clean(); wp_styles()->reset(); }; From 54b2474d703e4f00ee552fe39de40478c291a236 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Nov 2025 22:59:07 -0800 Subject: [PATCH 17/50] Refactor HTML Tag Processor logic --- src/wp-includes/script-loader.php | 93 ++++++++++++++----------------- 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 052259eeff6ca..8780bd975d732 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3726,81 +3726,72 @@ function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_ // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { - public function get_span(): WP_HTML_Span { - $this->set_bookmark( 'here' ); + /** + * Gets the span for the current token. + * + * @return WP_HTML_Span Current token span. + */ + private function get_span(): WP_HTML_Span { + $this->set_bookmark( 'here' ); // TODO: What if this fails? return $this->bookmarks['here']; } + + /** + * Inserts text before the current token. + * + * @param string $text Text to insert. + */ + public function insert_before( string $text ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text ); + } + + /** + * Inserts text after the current token. + * + * @param string $text Text to insert. + */ + public function insert_after( string $text ) { + $span = $this->get_span(); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text ); + } }; // TODO: If there are no block styles to print, it would be nice to not have to replace the placeholder comment. - // Locate the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment. - while ( $processor->next_tag( array( 'tag_name' => 'STYLE' ) ) ) { + // Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles at . + while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) { if ( - ! $processor->is_tag_closer() && 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { - // If the inline style lacks the placeholder comment, then all the styles will be inserted below before . + // If the inline style lacks the placeholder comment, then all the styles will be inserted below at . $css_text = $processor->get_modifiable_text(); if ( ! str_contains( $css_text, $placeholder ) ) { - break; + continue; } // Remove the placeholder now that we've located the inline style. $processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) ); - $buffer = $processor->get_updated_html(); // Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade. - $span = $processor->get_span(); - $buffer = implode( - '', - array( - substr( $buffer, 0, $span->start + $span->length ), - $printed_block_styles, - substr( $buffer, $span->start + $span->length ), - ) - ); - - // Prevent printing them again. - $printed_block_styles = ''; - break; - } - } + if ( '' !== $printed_block_styles ) { + $processor->insert_after( $printed_block_styles ); - // If there are remaining styles to hoist, either because - if ( $printed_block_styles || $printed_late_styles ) { - - // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. - $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { - public function get_span(): WP_HTML_Span { - $this->set_bookmark( 'here' ); - return $this->bookmarks['here']; + // Prevent printing them again at . + $printed_block_styles = ''; } - }; - - // As a fallback, append the hoisted late styles to the end of the HEAD. - while ( $processor->next_tag( - array( - 'tag_name' => 'HEAD', - 'tag_closers' => 'visit', - ) - ) ) { - if ( $processor->is_tag_closer() ) { - $span = $processor->get_span(); - $buffer = implode( - '', - array( - substr( $buffer, 0, $span->start ), - $printed_block_styles . $printed_late_styles, - substr( $buffer, $span->start ), - ) - ); + + // If there aren't any late styles, there's no need to continue to finding . + if ( '' === $printed_late_styles ) { break; } + } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) { + $processor->insert_before( $printed_block_styles . $printed_late_styles ); + break; } } - return $buffer; + return $processor->get_updated_html(); } ); } From d93e76707a9413bacc998726ef4071d479ceacfd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 11:27:53 -0800 Subject: [PATCH 18/50] Fix missing use of footer in assertion --- tests/phpunit/tests/template.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 1434cdec86786..0e87fb180461a 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1529,6 +1529,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'expected_footer' => array( 'late-css', 'late-inline-css', + 'core-block-supports-inline-css', ), ), 'classic_theme_opt_out_separate_block_styles_and_no_footer_printing' => array( @@ -1622,9 +1623,9 @@ function (): void { 'wp-custom-css', ), 'expected_footer' => array( - 'core-block-supports-inline-css', 'late-css', 'late-inline-css', + 'core-block-supports-inline-css', ), ), ); @@ -1737,8 +1738,8 @@ static function () { 'Expected the same styles in the HEAD. Snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) ); $this->assertSame( - $expected_head, - $found_styles['HEAD'], + $expected_footer, + $found_styles['BODY'], 'Expected the same styles in the BODY. Snapshot: ' . self::get_array_snapshot_export( $found_styles['BODY'] ) ); } From ec25a5f333e90da69f40314777650701493e8928 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 11:35:00 -0800 Subject: [PATCH 19/50] Use correct variable in assertion --- tests/phpunit/tests/template.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 0e87fb180461a..9c85ed7aa4d33 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1698,6 +1698,7 @@ static function () { wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null ); wp_add_inline_style( 'late', '/* LATE */' ); + // Simulate the_content(). $content = apply_filters( 'the_content', '
' @@ -1712,7 +1713,7 @@ static function () { // Apply the output buffer filter. $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer ); - $this->assertStringContainsString( '', $buffer, 'Expected the closing HEAD tag to be in the response.' ); + $this->assertStringContainsString( '', $filtered_buffer, 'Expected the closing HEAD tag to be in the response.' ); $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' ); $found_styles = array( From 53ef0c63195d23f06fc29f21b99e9e13da693a13 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 11:38:49 -0800 Subject: [PATCH 20/50] Remove unhelpful test cases since print_late_styles filter seems to have no effect --- tests/phpunit/tests/template.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 9c85ed7aa4d33..2ae0ca4a38120 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1532,27 +1532,6 @@ public function data_wp_hoist_late_printed_styles(): array { 'core-block-supports-inline-css', ), ), - 'classic_theme_opt_out_separate_block_styles_and_no_footer_printing' => array( - 'set_up' => static function () { - add_filter( 'should_load_separate_core_block_assets', '__return_false' ); - add_filter( 'print_late_styles', '__return_false' ); - }, - 'theme_supports' => $theme_supports, - 'expected_head' => array( - 'wp-img-auto-sizes-contain-inline-css', - 'early-css', - 'early-inline-css', - 'wp-emoji-styles-inline-css', - 'wp-block-library-css', - 'wp-block-library-theme-inline-css', - 'classic-theme-styles-inline-css', - 'global-styles-inline-css', - 'normal-css', - 'normal-inline-css', - 'wp-custom-css', - ), - 'expected_footer' => array(), - ), 'wp_block_styles_not_supported' => array( 'set_up' => null, 'theme_supports' => array(), @@ -1566,14 +1545,6 @@ public function data_wp_hoist_late_printed_styles(): array { ), 'expected_footer' => array(), ), - 'disabled_printing_late_styles' => array( - 'set_up' => static function () { - add_filter( 'print_late_styles', '__return_false', 1000 ); - }, - 'theme_supports' => $theme_supports, - 'expected_head' => $expected_head_styles, - 'expected_footer' => array(), - ), '_wp_footer_scripts_removed' => array( 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); From ead1aa2585df5f7abb2c95c59a12ff828ff8dcac Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 11:40:57 -0800 Subject: [PATCH 21/50] Set styles_inline_size_limit to unlimited for test --- tests/phpunit/tests/template.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 2ae0ca4a38120..32b9fc9183aa6 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1518,7 +1518,7 @@ public function data_wp_hoist_late_printed_styles(): array { 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', - 'wp-block-library-css', + 'wp-block-library-inline-css', 'wp-block-library-theme-inline-css', 'classic-theme-styles-inline-css', 'global-styles-inline-css', @@ -1620,6 +1620,14 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $them add_theme_support( $theme_support ); } + // Set the styles_inline_size_limit to unlimited in order to prevent changes from invalidating the snapshots. + add_filter( + 'styles_inline_size_limit', + static function (): int { + return PHP_INT_MAX; + } + ); + add_filter( 'wp_get_custom_css', static function () { From 27f5e33fd69fa3ec3f04e114c17c6c1b12c61f00 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 12:14:02 -0800 Subject: [PATCH 22/50] Opt to set styles_inline_size_limit to zero for test --- tests/phpunit/tests/template.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 32b9fc9183aa6..1f47082b693f3 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1487,12 +1487,12 @@ public function data_wp_hoist_late_printed_styles(): array { 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', - 'wp-block-library-inline-css', - 'wp-block-separator-inline-css', - 'wp-block-separator-theme-inline-css', + 'wp-block-library-css', + 'wp-block-separator-css', + 'wp-block-separator-theme-css', 'global-styles-inline-css', 'core-block-supports-inline-css', - 'classic-theme-styles-inline-css', + 'classic-theme-styles-css', 'normal-css', 'normal-inline-css', 'wp-custom-css', @@ -1518,9 +1518,9 @@ public function data_wp_hoist_late_printed_styles(): array { 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', - 'wp-block-library-inline-css', - 'wp-block-library-theme-inline-css', - 'classic-theme-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-theme-css', + 'classic-theme-styles-css', 'global-styles-inline-css', 'normal-css', 'normal-inline-css', @@ -1539,7 +1539,7 @@ public function data_wp_hoist_late_printed_styles(): array { array_diff( $expected_head_styles, array( - 'wp-block-separator-theme-inline-css', + 'wp-block-separator-theme-css', ) ) ), @@ -1587,7 +1587,7 @@ function (): void { 'early-css', 'early-inline-css', 'wp-emoji-styles-inline-css', - 'classic-theme-styles-inline-css', + 'classic-theme-styles-css', 'global-styles-inline-css', 'normal-css', 'normal-inline-css', @@ -1620,11 +1620,11 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $them add_theme_support( $theme_support ); } - // Set the styles_inline_size_limit to unlimited in order to prevent changes from invalidating the snapshots. + // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots. add_filter( 'styles_inline_size_limit', static function (): int { - return PHP_INT_MAX; + return 0; } ); From 3c77325823decc800997317d1aed8281c54de6e8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 12:20:52 -0800 Subject: [PATCH 23/50] Test that non-empty wp-block-library inline style is not removed --- src/wp-includes/script-loader.php | 32 +++++++++++++++++++++++++++---- tests/phpunit/tests/template.php | 19 ++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index d6ade9bf1b513..3509c715bbb71 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3749,10 +3749,26 @@ public function insert_after( string $text ) { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text ); } + + /** + * Removes the current token. + */ + public function remove() { + $span = $this->get_span(); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' ); + } }; - // TODO: If there are no block styles to print, it would be nice to not have to replace the placeholder comment. - // Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles at . + /* + * Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles + * at (or else print everything there). 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 are rendered. + * This means that there may not actually be any block styles to hoist from the footer to insert after this + * inline style. The placeholder CSS comment needs to be added so that the inline style gets printed, but + * if the resulting inline style is empty after the placeholder is removed, then the inline style is + * removed. + */ while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) { if ( 'STYLE' === $processor->get_tag() && @@ -3764,8 +3780,16 @@ public function insert_after( string $text ) { continue; } - // Remove the placeholder now that we've located the inline style. - $processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) ); + /* + * Remove the placeholder now that we've located the inline style (and remove the inline style if it + * is now empty, aside from a sourceURL comment). + */ + $css_text = str_replace( $placeholder, '', $css_text ); + if ( preg_match( ':^/\*# sourceURL=\S+? \*/$:', trim( $css_text ) ) ) { + $processor->remove(); + } else { + $processor->set_modifiable_text( $css_text ); + } // Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade. if ( '' !== $printed_block_styles ) { diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 1f47082b693f3..a71077bafe9f6 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1508,6 +1508,25 @@ public function data_wp_hoist_late_printed_styles(): array { 'expected_head' => $expected_head_styles, 'expected_footer' => array(), ), + 'standard_classic_theme_config_extra_block_library_inline_style' => 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. */' ); + } + ); + }, + 'theme_supports' => $theme_supports, + 'expected_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; + } )( $expected_head_styles ), + 'expected_footer' => array(), + ), 'classic_theme_opt_out_separate_block_styles' => array( 'set_up' => static function () { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); From 12d4ae75843b1547f0e94facd3a6e5578652a3c1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 12:28:59 -0800 Subject: [PATCH 24/50] Consolidate assertions --- tests/phpunit/tests/template.php | 149 ++++++++++++++++--------------- 1 file changed, 79 insertions(+), 70 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index a71077bafe9f6..04514fbb5aa27 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1475,14 +1475,14 @@ 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 { $theme_supports = array( 'wp-block-styles', ); - $expected_head_styles = array( + $common_expected_head_styles = array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', 'early-inline-css', @@ -1505,8 +1505,10 @@ public function data_wp_hoist_late_printed_styles(): array { 'standard_classic_theme_config' => array( 'set_up' => null, 'theme_supports' => $theme_supports, - 'expected_head' => $expected_head_styles, - 'expected_footer' => array(), + 'expected_styles' => array( + 'HEAD' => $common_expected_head_styles, + 'BODY' => array(), + ), ), 'standard_classic_theme_config_extra_block_library_inline_style' => array( 'set_up' => static function () { @@ -1518,67 +1520,77 @@ static function () { ); }, 'theme_supports' => $theme_supports, - 'expected_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; - } )( $expected_head_styles ), - 'expected_footer' => array(), + '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 ), + 'BODY' => array(), + ), ), 'classic_theme_opt_out_separate_block_styles' => array( 'set_up' => static function () { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, 'theme_supports' => $theme_supports, - 'expected_head' => array( - 'wp-img-auto-sizes-contain-inline-css', - 'early-css', - 'early-inline-css', - 'wp-emoji-styles-inline-css', - 'wp-block-library-css', - 'wp-block-library-theme-css', - 'classic-theme-styles-css', - 'global-styles-inline-css', - 'normal-css', - 'normal-inline-css', - 'wp-custom-css', - ), - 'expected_footer' => array( - 'late-css', - 'late-inline-css', - 'core-block-supports-inline-css', + 'expected_styles' => array( + 'HEAD' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-theme-css', + 'classic-theme-styles-css', + 'global-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + ), + 'BODY' => array( + 'late-css', + 'late-inline-css', + 'core-block-supports-inline-css', + ), ), ), 'wp_block_styles_not_supported' => array( 'set_up' => null, 'theme_supports' => array(), - 'expected_head' => array_values( - array_diff( - $expected_head_styles, - array( - 'wp-block-separator-theme-css', + 'expected_styles' => array( + 'HEAD' => array_values( + array_diff( + $common_expected_head_styles, + array( + 'wp-block-separator-theme-css', + ) ) - ) + ), + 'BODY' => array(), ), - 'expected_footer' => array(), ), '_wp_footer_scripts_removed' => array( 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, 'theme_supports' => $theme_supports, - 'expected_head' => $expected_head_styles, - 'expected_footer' => array(), + 'expected_styles' => array( + 'HEAD' => $common_expected_head_styles, + 'BODY' => array(), + ), ), 'wp_print_footer_scripts_removed' => array( 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, 'theme_supports' => $theme_supports, - 'expected_head' => $expected_head_styles, - 'expected_footer' => array(), + 'expected_styles' => array( + 'HEAD' => $common_expected_head_styles, + 'BODY' => array(), + ), ), 'both_actions_removed' => array( 'set_up' => static function () { @@ -1586,8 +1598,10 @@ static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, 'theme_supports' => $theme_supports, - 'expected_head' => $expected_head_styles, - 'expected_footer' => array(), + 'expected_styles' => array( + 'HEAD' => $common_expected_head_styles, + 'BODY' => array(), + ), ), 'disable_block_library' => array( 'set_up' => static function () { @@ -1601,21 +1615,23 @@ function (): void { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, 'theme_supports' => array(), - 'expected_head' => array( - 'wp-img-auto-sizes-contain-inline-css', - 'early-css', - 'early-inline-css', - 'wp-emoji-styles-inline-css', - 'classic-theme-styles-css', - 'global-styles-inline-css', - 'normal-css', - 'normal-inline-css', - 'wp-custom-css', - ), - 'expected_footer' => array( - 'late-css', - 'late-inline-css', - 'core-block-supports-inline-css', + 'expected_styles' => array( + 'HEAD' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'classic-theme-styles-css', + 'global-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + ), + 'BODY' => array( + 'late-css', + 'late-inline-css', + 'core-block-supports-inline-css', + ), ), ), ); @@ -1630,7 +1646,7 @@ function (): void { * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected_head, array $expected_footer ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected_styles ): void { switch_theme( 'default' ); global $wp_styles; $wp_styles = null; @@ -1732,14 +1748,9 @@ static function () { } $this->assertSame( - $expected_head, - $found_styles['HEAD'], - 'Expected the same styles in the HEAD. Snapshot: ' . self::get_array_snapshot_export( $found_styles['HEAD'] ) - ); - $this->assertSame( - $expected_footer, - $found_styles['BODY'], - 'Expected the same styles in the BODY. Snapshot: ' . self::get_array_snapshot_export( $found_styles['BODY'] ) + $expected_styles, + $found_styles, + 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_styles ) ); } @@ -1774,7 +1785,6 @@ public function assertTemplateHierarchy( $url, array $expected, $message = '' ) * 'c', * ) * - * This does not currently support nested arrays. * * @param array $snapshot Snapshot. * @return string Snapshot export. @@ -1782,9 +1792,8 @@ public function assertTemplateHierarchy( $url, array $expected, $message = '' ) private static function get_array_snapshot_export( array $snapshot ): string { $export = var_export( $snapshot, true ); $export = preg_replace( '/\barray \($/m', 'array(', $export ); - if ( array_is_list( $snapshot ) ) { - $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export ); - } + $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export ); + $export = preg_replace( '/=> *\n +/', '=> ', $export ); return preg_replace_callback( '/(^ +)/m', static function ( $matches ) { From 1e5797c5fe020cbd71b991eb1e4bce983edbf188 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 12:50:51 -0800 Subject: [PATCH 25/50] Fix classic_theme_with_should_load_separate_core_block_assets_opt_out test case --- tests/phpunit/tests/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 04514fbb5aa27..f3348fefbc8a4 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1422,7 +1422,7 @@ public function data_wp_load_classic_theme_block_styles_on_demand(): array { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, 'expected_load_separate' => false, - 'expected_on_demand' => true, + 'expected_on_demand' => false, 'expected_buffer_started' => false, ), 'classic_theme_with_should_load_block_assets_on_demand_out_out' => array( From 08f4809571fdc47b7da2c8cd563f5e28a5d35a78 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 13:21:37 -0800 Subject: [PATCH 26/50] Empty out added theme supports before running tests --- tests/phpunit/tests/template.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index f3348fefbc8a4..a35e6acb21096 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -122,7 +122,7 @@ public function set_up() { remove_filter( 'should_load_block_assets_on_demand', '__return_true', 0 ); remove_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ); - global $wp_scripts, $wp_styles; + global $wp_scripts, $wp_styles, $_wp_theme_features; $this->original_wp_scripts = $wp_scripts; $this->original_wp_styles = $wp_styles; $wp_scripts = null; @@ -130,18 +130,19 @@ public function set_up() { wp_scripts(); wp_styles(); - $this->original_theme_features = $GLOBALS['_wp_theme_features']; + $this->original_theme_features = $_wp_theme_features; + $_wp_theme_features = array(); foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { $this->original_ini_config[ $option ] = ini_get( $option ); } } public function tear_down() { - global $wp_scripts, $wp_styles; + global $wp_scripts, $wp_styles, $_wp_theme_features; $wp_scripts = $this->original_wp_scripts; $wp_styles = $this->original_wp_styles; - $GLOBALS['_wp_theme_features'] = $this->original_theme_features; + $_wp_theme_features = $this->original_theme_features; foreach ( $this->original_ini_config as $option => $value ) { ini_set( $option, $value ); } From a9a54cab962111943541e84da30c7b518cb55818 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 14:22:15 -0800 Subject: [PATCH 27/50] Add core-block-supports-duotone to the list of styles to print after wp-block-library --- 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 3509c715bbb71..4bd42867686db 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3663,7 +3663,7 @@ function wp_hoist_late_printed_styles() { } $all_block_style_handles = array_merge( $all_block_style_handles, - array( 'global-styles', 'core-block-supports', 'block-style-variation-styles' ) // TODO: What else? + array( 'global-styles', 'core-block-supports', 'block-style-variation-styles', 'core-block-supports-duotone' ) // TODO: What else? ); $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) ); From c7a4c6451da8c1377800cd85f989d9af796bfdd5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 14:23:24 -0800 Subject: [PATCH 28/50] Improve formatting of empty array in export --- tests/phpunit/tests/template.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index a35e6acb21096..08d5b9ab1f088 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -131,7 +131,7 @@ public function set_up() { wp_styles(); $this->original_theme_features = $_wp_theme_features; - $_wp_theme_features = array(); + $_wp_theme_features = array(); foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { $this->original_ini_config[ $option ] = ini_get( $option ); } @@ -1795,6 +1795,7 @@ private static function get_array_snapshot_export( array $snapshot ): string { $export = preg_replace( '/\barray \($/m', 'array(', $export ); $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export ); $export = preg_replace( '/=> *\n +/', '=> ', $export ); + $export = preg_replace( '/array\(\n\s+\)/', 'array()', $export ); return preg_replace_callback( '/(^ +)/m', static function ( $matches ) { From a8fe022954b7c1e0d979b086baec8346e4392d57 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 14:23:50 -0800 Subject: [PATCH 29/50] Restore original block type registry after test --- tests/phpunit/tests/template.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 08d5b9ab1f088..57e8f35273e55 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -78,6 +78,11 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { */ protected $original_theme_features; + /** + * @var WP_Block_Type_Registry + */ + protected $original_block_type_registry; + /** * @var array */ @@ -135,6 +140,8 @@ public function set_up() { foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { $this->original_ini_config[ $option ] = ini_get( $option ); } + + $this->original_block_type_registry = WP_Block_Type_Registry::get_instance(); } public function tear_down() { @@ -150,6 +157,14 @@ public function tear_down() { unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); + + $reflection_class = new ReflectionClass( WP_Block_Type_Registry::class ); + $instance_property = $reflection_class->getProperty( 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + $instance_property->setAccessible( true ); + } + $instance_property->setValue( null, $this->original_block_type_registry ); + parent::tear_down(); } From 0ad3084e0fcb382607bf6b89aa018b1a73a8617d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:00:31 -0800 Subject: [PATCH 30/50] Fix tests when run as part of enture test suite --- tests/phpunit/tests/template.php | 64 ++++++++------------------------ 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 57e8f35273e55..5b37e77fc7261 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -78,11 +78,6 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { */ protected $original_theme_features; - /** - * @var WP_Block_Type_Registry - */ - protected $original_block_type_registry; - /** * @var array */ @@ -127,7 +122,7 @@ public function set_up() { remove_filter( 'should_load_block_assets_on_demand', '__return_true', 0 ); remove_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ); - global $wp_scripts, $wp_styles, $_wp_theme_features; + global $wp_scripts, $wp_styles; $this->original_wp_scripts = $wp_scripts; $this->original_wp_styles = $wp_styles; $wp_scripts = null; @@ -135,36 +130,20 @@ public function set_up() { wp_scripts(); wp_styles(); - $this->original_theme_features = $_wp_theme_features; - $_wp_theme_features = array(); foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { $this->original_ini_config[ $option ] = ini_get( $option ); } - - $this->original_block_type_registry = WP_Block_Type_Registry::get_instance(); } public function tear_down() { - global $wp_scripts, $wp_styles, $_wp_theme_features; + global $wp_scripts, $wp_styles; $wp_scripts = $this->original_wp_scripts; $wp_styles = $this->original_wp_styles; - $_wp_theme_features = $this->original_theme_features; - foreach ( $this->original_ini_config as $option => $value ) { - ini_set( $option, $value ); - } - unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); - $reflection_class = new ReflectionClass( WP_Block_Type_Registry::class ); - $instance_property = $reflection_class->getProperty( 'instance' ); - if ( PHP_VERSION_ID < 80100 ) { - $instance_property->setAccessible( true ); - } - $instance_property->setValue( null, $this->original_block_type_registry ); - parent::tear_down(); } @@ -1491,13 +1470,9 @@ 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 { - $theme_supports = array( - 'wp-block-styles', - ); - $common_expected_head_styles = array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', @@ -1505,7 +1480,6 @@ public function data_wp_hoist_late_printed_styles(): array { 'wp-emoji-styles-inline-css', 'wp-block-library-css', 'wp-block-separator-css', - 'wp-block-separator-theme-css', 'global-styles-inline-css', 'core-block-supports-inline-css', 'classic-theme-styles-css', @@ -1520,7 +1494,6 @@ public function data_wp_hoist_late_printed_styles(): array { // TODO: Add test case for embed template. 'standard_classic_theme_config' => array( 'set_up' => null, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), @@ -1535,7 +1508,6 @@ static function () { } ); }, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => ( function ( $expected_styles ) { // Insert 'wp-block-library-inline-css' right after 'wp-block-library-css'. @@ -1551,7 +1523,6 @@ static function () { 'set_up' => static function () { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => array( 'wp-img-auto-sizes-contain-inline-css', @@ -1559,7 +1530,6 @@ static function () { 'early-inline-css', 'wp-emoji-styles-inline-css', 'wp-block-library-css', - 'wp-block-library-theme-css', 'classic-theme-styles-css', 'global-styles-inline-css', 'normal-css', @@ -1575,7 +1545,6 @@ static function () { ), 'wp_block_styles_not_supported' => array( 'set_up' => null, - 'theme_supports' => array(), 'expected_styles' => array( 'HEAD' => array_values( array_diff( @@ -1592,7 +1561,6 @@ static function () { 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), @@ -1602,7 +1570,6 @@ static function () { 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), @@ -1613,7 +1580,6 @@ static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, - 'theme_supports' => $theme_supports, 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), @@ -1630,7 +1596,6 @@ function (): void { ); add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, - 'theme_supports' => array(), 'expected_styles' => array( 'HEAD' => array( 'wp-img-auto-sizes-contain-inline-css', @@ -1662,15 +1627,11 @@ function (): void { * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $theme_supports, array $expected_styles ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $expected_styles ): void { switch_theme( 'default' ); global $wp_styles; $wp_styles = null; - foreach ( $theme_supports as $theme_support ) { - add_theme_support( $theme_support ); - } - // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots. add_filter( 'styles_inline_size_limit', @@ -1692,12 +1653,7 @@ static function () { wp_load_classic_theme_block_styles_on_demand(); - $block_registry = WP_Block_Type_Registry::get_instance(); - foreach ( array_keys( $block_registry->get_all_registered() ) as $block_name ) { - $block_registry->unregister( $block_name ); - } register_core_block_style_handles(); - register_core_block_types_from_metadata(); // See register_block_type_from_metadata(). $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); @@ -1763,9 +1719,19 @@ static function () { } } + /* + * Since new styles could appear at any time and since certain styles leak in from the global scope not being + * properly reset somewhere else in the test suite, we only check that the expected styles are at least present + * and in the same order. + */ + $found_subset_styles = array(); + foreach ( array( 'HEAD', 'BODY' ) as $group ) { + $found_subset_styles[ $group ] = array_values( array_intersect( $found_styles[ $group ], $expected_styles[ $group ] ) ); + } + $this->assertSame( $expected_styles, - $found_styles, + $found_subset_styles, 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_styles ) ); } From e3e5b3bc07cf2ed09ffd687e1020274d38d628c9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:01:59 -0800 Subject: [PATCH 31/50] Revert unrelated WP_Block_Type_Registry change --- src/wp-includes/class-wp-block-type-registry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block-type-registry.php b/src/wp-includes/class-wp-block-type-registry.php index 0751aa7c92902..969f0f0f64a42 100644 --- a/src/wp-includes/class-wp-block-type-registry.php +++ b/src/wp-includes/class-wp-block-type-registry.php @@ -150,7 +150,7 @@ public function get_registered( $name ) { * * @since 5.0.0 * - * @return array Associative array of `$block_type_name => $block_type` pairs. + * @return WP_Block_Type[] Associative array of `$block_type_name => $block_type` pairs. */ public function get_all_registered() { return $this->registered_block_types; From 9cc527e14879a9387d2c44f87755fc0450f66955 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:14:27 -0800 Subject: [PATCH 32/50] Restore original_ini_config --- tests/phpunit/tests/template.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 5b37e77fc7261..0d245539610d0 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -140,6 +140,10 @@ public function tear_down() { $wp_scripts = $this->original_wp_scripts; $wp_styles = $this->original_wp_styles; + foreach ( $this->original_ini_config as $option => $value ) { + ini_set( $option, $value ); + } + unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); From dac85dfb8f23d21461ff3a3ca486bb37f868fa39 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:16:58 -0800 Subject: [PATCH 33/50] Remove admin-specific style printing logic --- src/wp-includes/script-loader.php | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 4bd42867686db..dcb654cd65acf 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2264,11 +2264,6 @@ function wp_print_head_scripts() { /** * Private, for use in *_footer_scripts hooks * - * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()}, - * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of - * {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted - * to the HEAD by means of the template enhancement output buffer. - * * @since 3.3.0 */ function _wp_footer_scripts() { @@ -3651,10 +3646,8 @@ function wp_hoist_late_printed_styles() { $printed_block_styles = ''; $printed_late_styles = ''; $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) { - global $concatenate_scripts; - script_concat_settings(); - // Print the styles related to on-demand block enqueues. + // Gather the styles related to on-demand block enqueues. $all_block_style_handles = array(); foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { foreach ( $block_type->style_handles as $style_handle ) { @@ -3663,29 +3656,33 @@ function wp_hoist_late_printed_styles() { } $all_block_style_handles = array_merge( $all_block_style_handles, - array( 'global-styles', 'core-block-supports', 'block-style-variation-styles', 'core-block-supports-duotone' ) // TODO: What else? + array( + 'global-styles', + 'block-style-variation-styles', + 'core-block-supports', + 'core-block-supports-duotone', + ) ); + /* + * First print all styles related to blocks which should 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_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) ); if ( count( $enqueued_block_styles ) > 0 ) { - wp_styles()->do_concat = $concatenate_scripts; ob_start(); wp_styles()->do_items( $enqueued_block_styles ); - _print_styles(); // TODO: Is this needed? $printed_block_styles = ob_get_clean(); - wp_styles()->reset(); } /* - * Print remaining styles not related to blocks. This is the same logic as in print_late_styles(), but without - * the filter to control whether late styles are printed (since they are being hoisted anyway). + * Print all remaining styles not related to blocks. This contains a subset of the logic from + * `print_late_styles()`, without admin-specific logic and the `print_late_styles` filter to control whether + * late styles are printed (since they are being hoisted anyway). */ - wp_styles()->do_concat = $concatenate_scripts; ob_start(); wp_styles()->do_footer_items(); - _print_styles(); // TODO: Is this needed? $printed_late_styles = ob_get_clean(); - wp_styles()->reset(); }; /* From 0d65fcab2ad8a5a19ea934737d0ee72681ee1f5d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:24:57 -0800 Subject: [PATCH 34/50] Carify comments --- src/wp-includes/script-loader.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index dcb654cd65acf..51682e5fdc0fe 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3642,11 +3642,15 @@ function wp_hoist_late_printed_styles() { wp_add_inline_style( 'wp-block-library', $placeholder ); - // Wrap print_late_styles() with a closure that captures the late-printed styles. + /* + * Create a substitute for `print_late_styles()` which is aware of block styles. This substitute does not print + * the styles, but it captures what would be printed for block styles and non-block styles so that they can be + * later hoisted to the HEAD in the template enhancement output buffer. This will run at `wp_print_footer_scripts` + * before `print_footer_scripts()` is called. + */ $printed_block_styles = ''; $printed_late_styles = ''; $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) { - // Gather the styles related to on-demand block enqueues. $all_block_style_handles = array(); foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { @@ -3686,12 +3690,12 @@ function wp_hoist_late_printed_styles() { }; /* - * If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts() - * was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the + * If `_wp_footer_scripts()` was unhooked from the `wp_print_footer_scripts` action, or if `wp_print_footer_scripts()` + * was unhooked from running at the `wp_footer` action, then only add a callback to `wp_footer` which will capture the * late-printed styles. * - * Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then - * swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before + * Otherwise, in the normal case where `_wp_footer_scripts()` will run at the `wp_print_footer_scripts` action, then + * swap out `_wp_footer_scripts()` with an alternative which captures the printed styles (for hoisting to HEAD) before * proceeding with printing the footer scripts. */ $wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); From 5d227710c246912a00a86b7a4bd1204d85791fb8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:31:25 -0800 Subject: [PATCH 35/50] Add test for when styles_inline_size_limit is unlimited --- tests/phpunit/tests/template.php | 74 +++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 0d245539610d0..9312bdfc7ca51 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1474,7 +1474,7 @@ 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 { $common_expected_head_styles = array( @@ -1496,15 +1496,39 @@ public function data_wp_hoist_late_printed_styles(): array { return array( // TODO: Add test case for embed template. - 'standard_classic_theme_config' => array( - 'set_up' => null, - 'expected_styles' => array( + 'standard_classic_theme_config_with_min_styles_inlined' => array( + 'set_up' => null, + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), ), ), + 'standard_classic_theme_config_with_max_styles_inlined' => array( + 'set_up' => null, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'global-styles-inline-css', + 'core-block-supports-inline-css', + 'classic-theme-styles-inline-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + 'late-css', + 'late-inline-css', + ), + 'BODY' => array(), + ), + ), 'standard_classic_theme_config_extra_block_library_inline_style' => array( - 'set_up' => static function () { + 'set_up' => static function () { add_action( 'enqueue_block_assets', static function () { @@ -1512,7 +1536,8 @@ static function () { } ); }, - 'expected_styles' => array( + '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 ); @@ -1524,10 +1549,11 @@ static function () { ), ), 'classic_theme_opt_out_separate_block_styles' => array( - 'set_up' => static function () { + 'set_up' => static function () { add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, - 'expected_styles' => array( + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', @@ -1548,8 +1574,9 @@ static function () { ), ), 'wp_block_styles_not_supported' => array( - 'set_up' => null, - 'expected_styles' => array( + 'set_up' => null, + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => array_values( array_diff( $common_expected_head_styles, @@ -1562,35 +1589,38 @@ static function () { ), ), '_wp_footer_scripts_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); }, - 'expected_styles' => array( + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), ), ), 'wp_print_footer_scripts_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); }, - 'expected_styles' => array( + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), ), ), 'both_actions_removed' => array( - 'set_up' => static function () { + 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); remove_action( 'wp_footer', 'wp_print_footer_scripts' ); }, - 'expected_styles' => array( + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => $common_expected_head_styles, 'BODY' => array(), ), ), 'disable_block_library' => array( - 'set_up' => static function () { + 'set_up' => static function () { add_action( 'enqueue_block_assets', function (): void { @@ -1600,7 +1630,8 @@ function (): void { ); add_filter( 'should_load_separate_core_block_assets', '__return_false' ); }, - 'expected_styles' => array( + 'inline_size_limit' => 0, + 'expected_styles' => array( 'HEAD' => array( 'wp-img-auto-sizes-contain-inline-css', 'early-css', @@ -1631,7 +1662,7 @@ function (): void { * * @dataProvider data_wp_hoist_late_printed_styles */ - public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $expected_styles ): void { + public function test_wp_hoist_late_printed_styles( ?Closure $set_up, int $inline_size_limit, array $expected_styles ): void { switch_theme( 'default' ); global $wp_styles; $wp_styles = null; @@ -1639,8 +1670,8 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, array $expe // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots. add_filter( 'styles_inline_size_limit', - static function (): int { - return 0; + static function () use ( $inline_size_limit ): int { + return $inline_size_limit; } ); @@ -1657,6 +1688,7 @@ static function () { wp_load_classic_theme_block_styles_on_demand(); + // Ensure that separate core block assets get registered. register_core_block_style_handles(); $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); From 56c35c83a97a8bf8d4faa1f65a333a667a548e56 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:32:22 -0800 Subject: [PATCH 36/50] Remove obsolete wp_block_styles_not_supported test --- tests/phpunit/tests/template.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 9312bdfc7ca51..38f7737bcce38 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1573,21 +1573,6 @@ static function () { ), ), ), - 'wp_block_styles_not_supported' => array( - 'set_up' => null, - 'inline_size_limit' => 0, - 'expected_styles' => array( - 'HEAD' => array_values( - array_diff( - $common_expected_head_styles, - array( - 'wp-block-separator-theme-css', - ) - ) - ), - 'BODY' => array(), - ), - ), '_wp_footer_scripts_removed' => array( 'set_up' => static function () { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); From 73c5b0748c607b3599b868874da1af8b11456fcf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:36:08 -0800 Subject: [PATCH 37/50] Remove test for test which is not worthwhile --- tests/phpunit/tests/template.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 38f7737bcce38..0ae20990803db 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1495,7 +1495,6 @@ public function data_wp_hoist_late_printed_styles(): array { ); return array( - // TODO: Add test case for embed template. 'standard_classic_theme_config_with_min_styles_inlined' => array( 'set_up' => null, 'inline_size_limit' => 0, From 18691a4ee64a52e68496d521ec779a838ca2e333 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:44:21 -0800 Subject: [PATCH 38/50] Replace todo with explanation of why set_bookmark will never return false --- src/wp-includes/script-loader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 51682e5fdc0fe..2b176a2fb0575 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3727,7 +3727,8 @@ function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_ * @return WP_HTML_Span Current token span. */ private function get_span(): WP_HTML_Span { - $this->set_bookmark( 'here' ); // TODO: What if this fails? + // Note: This call will never fail according to the usage of this class, given it is always called after ::next_tag() is true. + $this->set_bookmark( 'here' ); return $this->bookmarks['here']; } From 3f0ac523a3b185ddf910a918a3e0dad8fed3012a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:49:44 -0800 Subject: [PATCH 39/50] Restore comment to _wp_footer_scripts() --- src/wp-includes/script-loader.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 2b176a2fb0575..6205bb250fd6f 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2264,6 +2264,11 @@ function wp_print_head_scripts() { /** * Private, for use in *_footer_scripts hooks * + * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()}, + * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of + * {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted + * to the HEAD by means of the template enhancement output buffer. + * * @since 3.3.0 */ function _wp_footer_scripts() { From 34d7573818fdf17db65c144d6adfef214eef6bab Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 15:55:46 -0800 Subject: [PATCH 40/50] Restore and update comment for _wp_footer_scripts() --- src/wp-includes/script-loader.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 6205bb250fd6f..26facbb8afe1a 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2265,9 +2265,13 @@ function wp_print_head_scripts() { * Private, for use in *_footer_scripts hooks * * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()}, - * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of - * {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted - * to the HEAD by means of the template enhancement output buffer. + * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the printing of + * two sets of "late" styles to be hoisted to the HEAD by means of the template enhancement output buffer: + * + * 1. Styles related to blocks are inserted right after the wp-block-library stylesheet. + * 2. All other styles are appended to the end of the HEAD. + * + * The closure calls {@see print_footer_scripts()} to print scripts in the footer as usual. * * @since 3.3.0 */ From 252438625feceafafa50b1292e7e04fd19724a9e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 16:01:10 -0800 Subject: [PATCH 41/50] Add assertion to ensure wp-block-separator style is registered --- 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 0ae20990803db..fd983e0f3be2b 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1674,6 +1674,9 @@ static function () { // Ensure that separate core block assets get registered. register_core_block_style_handles(); + if ( wp_should_load_separate_core_block_assets() ) { + $this->assertTrue( wp_style_is( 'wp-block-separator', 'registered' ), 'Expected the wp-block-separator style to be registered.' ); + } $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); From fd0c4024aa29a4bbcf8d17eed1b2ca4469e22157 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 16:34:41 -0800 Subject: [PATCH 42/50] Debug --- .github/workflows/phpunit-tests.yml | 10 ++-- src/wp-includes/class-wp-block.php | 9 ++++ src/wp-includes/script-loader.php | 6 +++ tests/phpunit/tests/template.php | 84 +++++++++++++++++++++++++++-- 4 files changed, 101 insertions(+), 8 deletions(-) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 0deed7ac79aa8..bdfa272869c67 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -69,7 +69,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: false + fail-fast: true matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -205,7 +205,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: false + fail-fast: true matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -428,7 +428,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: false + fail-fast: true matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -495,7 +495,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: false + fail-fast: true matrix: php: [ '7.2', '7.4', '8.0', '8.4' ] db-type: [ 'mysql' ] @@ -524,7 +524,7 @@ jobs: secrets: inherit if: ${{ ! startsWith( github.repository, 'WordPress/' ) && github.event_name == 'pull_request' }} strategy: - fail-fast: false + fail-fast: true matrix: php: [ '7.2', '8.4' ] db-version: [ '8.4', '11.8' ] diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 5188246180dd7..5c2a6ea298ac9 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -628,6 +628,9 @@ public function render( $options = array() ) { */ if ( ( ! empty( $this->block_type->style_handles ) ) ) { foreach ( $this->block_type->style_handles as $style_handle ) { + if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { + error_log( __FILE__ . ':' . __LINE__ . ' enqueue: ' . json_encode( $style_handle ) ); + } wp_enqueue_style( $style_handle ); } } @@ -675,6 +678,9 @@ public function render( $options = array() ) { $after_styles_queue = wp_styles()->queue; $after_scripts_queue = wp_scripts()->queue; $after_script_modules_queue = wp_script_modules()->get_queue(); + if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { + error_log( __FILE__ . ':' . __LINE__ . ' $after_styles_queue: ' . json_encode( $after_styles_queue ) ); + } /* * As a very special case, a dynamic block may in fact include a call to wp_head() (and thus wp_enqueue_scripts()), @@ -706,6 +712,9 @@ public function render( $options = array() ) { ) { foreach ( array_diff( $after_styles_queue, $before_styles_queue ) as $handle ) { wp_dequeue_style( $handle ); + if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { + error_log( __FILE__ . ':' . __LINE__ . ' wp_dequeue_style: ' . json_encode( $handle ) ); + } } foreach ( array_diff( $after_scripts_queue, $before_scripts_queue ) as $handle ) { wp_dequeue_script( $handle ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 26facbb8afe1a..744388cefc652 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3660,6 +3660,8 @@ function wp_hoist_late_printed_styles() { $printed_block_styles = ''; $printed_late_styles = ''; $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) { + error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->queue: ' . json_encode( wp_styles()->queue ) ); + // Gather the styles related to on-demand block enqueues. $all_block_style_handles = array(); foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { @@ -3682,11 +3684,14 @@ function wp_hoist_late_printed_styles() { * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`. */ $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) ); + error_log( __FILE__ . ':' . __LINE__ . ' $enqueued_block_styles: ' . json_encode( $enqueued_block_styles ) ); + error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); if ( count( $enqueued_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_block_styles ); $printed_block_styles = ob_get_clean(); } + error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); /* * Print all remaining styles not related to blocks. This contains a subset of the logic from @@ -3696,6 +3701,7 @@ function wp_hoist_late_printed_styles() { ob_start(); wp_styles()->do_footer_items(); $printed_late_styles = ob_get_clean(); + error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); }; /* diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index fd983e0f3be2b..204c6462e62b2 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -127,8 +127,6 @@ public function set_up() { $this->original_wp_styles = $wp_styles; $wp_scripts = null; $wp_styles = null; - wp_scripts(); - wp_styles(); foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { $this->original_ini_config[ $option ] = ini_get( $option ); @@ -1647,6 +1645,9 @@ 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 { + $GLOBALS['debug_on_demand_block_style'] = true; // TODO: Remove. + error_log( "\n##BEGIN DEBUG ####################################\n" ); + switch_theme( 'default' ); global $wp_styles; $wp_styles = null; @@ -1674,8 +1675,49 @@ static function () { // Ensure that separate core block assets get registered. register_core_block_style_handles(); + $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' ); + + // Ensure + $this->ensure_style_asset_file_created( 'wp-block-library', 'css/dist/block-library/style.css' ); +// $handle = 'wp-block-library'; +// $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-library style to be registered.' ); +// $dependency = wp_styles()->query( $handle ); +// $relative_path = 'css/dist/block-library/style.css'; +// error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); +// $dependency->src = includes_url( $relative_path ); +// $path = ABSPATH . WPINC . '/blocks/separator/style.css'; +// if ( ! file_exists( $path ) ) { +// mkdir( dirname( $path ), 0777, true ); +// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); +// file_put_contents( $path, '/* The separator CSS */' ); +// } else { +// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); +// } +// wp_style_add_data( $handle, 'path', $path ); + if ( wp_should_load_separate_core_block_assets() ) { - $this->assertTrue( wp_style_is( 'wp-block-separator', 'registered' ), 'Expected the wp-block-separator style to be registered.' ); + $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' ); + +// $handle = 'wp-block-separator'; +// $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-separator style to be registered.' ); +// $dependency = wp_styles()->query( $handle ); +// error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); +// $relative_path = 'blocks/separator/style.css'; +// $dependency->src = includes_url( $relative_path ); +// $path = ABSPATH . WPINC . '/' . $relative_path; +// if ( ! file_exists( $path ) ) { +// mkdir( dirname( $path ), 0777, true ); +// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); +// file_put_contents( $path, '/* The separator CSS */' ); +// } else { +// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); +// } +// wp_style_add_data( $handle, 'path', $path ); + + $handle = 'wp-block-separator'; + $done = wp_styles()->done; + error_log( __FILE__ . ':' . __LINE__ . ": wp_print_styles($handle): " . get_echo( 'wp_print_styles', array( $handle ) ) ); + wp_styles()->done = $done; } $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); @@ -1712,6 +1754,9 @@ static function () { 'the_content', '
' ); + if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { + error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->queue: ' . json_encode( wp_styles()->queue ) ); + } // Simulate footer scripts. $footer_output = get_echo( 'wp_footer' ); @@ -1757,6 +1802,39 @@ static function () { $found_subset_styles, 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_styles ) ); + + unset( $GLOBALS['debug_on_demand_block_style'] ); // TODO: Remove. + error_log( "\n##END DEBUG ####################################\n" ); // TODO: Remove. + } + + /** + * Ensures a CSS file is on the filesystem. + * + * This is needed because unit tests may be run without a build step having been done. Something similar can be seen + * elsewhere in tests for the `wp-emoji-loader.js` script: + * + * self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); + * + * @param string $handle Style handle. + * @param string $relative_path Relative path to the CSS file in the includes directory. + */ + private function ensure_style_asset_file_created( string $handle, string $relative_path ) { + $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-separator style to be registered.' ); + $dependency = wp_styles()->query( $handle ); + error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); + $dependency->src = includes_url( $relative_path ); + $path = ABSPATH . WPINC . '/' . $relative_path; + if ( ! file_exists( $path ) ) { + $dir = dirname( $path ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true ); + } + error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); + file_put_contents( $path, "/* CSS for $handle */" ); + } else { + error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); + } + wp_style_add_data( $handle, 'path', $path ); } public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { From c2fd29ff3230faa388b6d6cd1dee9110f3c18f63 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 5 Nov 2025 19:07:07 -0800 Subject: [PATCH 43/50] Undebug --- .github/workflows/phpunit-tests.yml | 10 +++--- src/wp-includes/class-wp-block.php | 9 ----- src/wp-includes/script-loader.php | 6 ---- tests/phpunit/tests/template.php | 54 ++--------------------------- 4 files changed, 7 insertions(+), 72 deletions(-) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index bdfa272869c67..0deed7ac79aa8 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -69,7 +69,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: true + fail-fast: false matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -205,7 +205,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: true + fail-fast: false matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -428,7 +428,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: true + fail-fast: false matrix: event: ['${{ github.event_name }}'] os: [ ubuntu-24.04 ] @@ -495,7 +495,7 @@ jobs: secrets: inherit if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: - fail-fast: true + fail-fast: false matrix: php: [ '7.2', '7.4', '8.0', '8.4' ] db-type: [ 'mysql' ] @@ -524,7 +524,7 @@ jobs: secrets: inherit if: ${{ ! startsWith( github.repository, 'WordPress/' ) && github.event_name == 'pull_request' }} strategy: - fail-fast: true + fail-fast: false matrix: php: [ '7.2', '8.4' ] db-version: [ '8.4', '11.8' ] diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 5c2a6ea298ac9..5188246180dd7 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -628,9 +628,6 @@ public function render( $options = array() ) { */ if ( ( ! empty( $this->block_type->style_handles ) ) ) { foreach ( $this->block_type->style_handles as $style_handle ) { - if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { - error_log( __FILE__ . ':' . __LINE__ . ' enqueue: ' . json_encode( $style_handle ) ); - } wp_enqueue_style( $style_handle ); } } @@ -678,9 +675,6 @@ public function render( $options = array() ) { $after_styles_queue = wp_styles()->queue; $after_scripts_queue = wp_scripts()->queue; $after_script_modules_queue = wp_script_modules()->get_queue(); - if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { - error_log( __FILE__ . ':' . __LINE__ . ' $after_styles_queue: ' . json_encode( $after_styles_queue ) ); - } /* * As a very special case, a dynamic block may in fact include a call to wp_head() (and thus wp_enqueue_scripts()), @@ -712,9 +706,6 @@ public function render( $options = array() ) { ) { foreach ( array_diff( $after_styles_queue, $before_styles_queue ) as $handle ) { wp_dequeue_style( $handle ); - if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { - error_log( __FILE__ . ':' . __LINE__ . ' wp_dequeue_style: ' . json_encode( $handle ) ); - } } foreach ( array_diff( $after_scripts_queue, $before_scripts_queue ) as $handle ) { wp_dequeue_script( $handle ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 744388cefc652..26facbb8afe1a 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3660,8 +3660,6 @@ function wp_hoist_late_printed_styles() { $printed_block_styles = ''; $printed_late_styles = ''; $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) { - error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->queue: ' . json_encode( wp_styles()->queue ) ); - // Gather the styles related to on-demand block enqueues. $all_block_style_handles = array(); foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { @@ -3684,14 +3682,11 @@ function wp_hoist_late_printed_styles() { * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`. */ $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) ); - error_log( __FILE__ . ':' . __LINE__ . ' $enqueued_block_styles: ' . json_encode( $enqueued_block_styles ) ); - error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); if ( count( $enqueued_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_block_styles ); $printed_block_styles = ob_get_clean(); } - error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); /* * Print all remaining styles not related to blocks. This contains a subset of the logic from @@ -3701,7 +3696,6 @@ function wp_hoist_late_printed_styles() { ob_start(); wp_styles()->do_footer_items(); $printed_late_styles = ob_get_clean(); - error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->done: ' . json_encode( wp_styles()->done ) ); }; /* diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 204c6462e62b2..99789ecb87527 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1645,9 +1645,6 @@ 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 { - $GLOBALS['debug_on_demand_block_style'] = true; // TODO: Remove. - error_log( "\n##BEGIN DEBUG ####################################\n" ); - switch_theme( 'default' ); global $wp_styles; $wp_styles = null; @@ -1677,47 +1674,10 @@ static function () { register_core_block_style_handles(); $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' ); - // Ensure + // Ensure stylesheet files exist on the filesystem since a build may not have been done. $this->ensure_style_asset_file_created( 'wp-block-library', 'css/dist/block-library/style.css' ); -// $handle = 'wp-block-library'; -// $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-library style to be registered.' ); -// $dependency = wp_styles()->query( $handle ); -// $relative_path = 'css/dist/block-library/style.css'; -// error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); -// $dependency->src = includes_url( $relative_path ); -// $path = ABSPATH . WPINC . '/blocks/separator/style.css'; -// if ( ! file_exists( $path ) ) { -// mkdir( dirname( $path ), 0777, true ); -// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); -// file_put_contents( $path, '/* The separator CSS */' ); -// } else { -// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); -// } -// wp_style_add_data( $handle, 'path', $path ); - if ( wp_should_load_separate_core_block_assets() ) { $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' ); - -// $handle = 'wp-block-separator'; -// $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-separator style to be registered.' ); -// $dependency = wp_styles()->query( $handle ); -// error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); -// $relative_path = 'blocks/separator/style.css'; -// $dependency->src = includes_url( $relative_path ); -// $path = ABSPATH . WPINC . '/' . $relative_path; -// if ( ! file_exists( $path ) ) { -// mkdir( dirname( $path ), 0777, true ); -// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); -// file_put_contents( $path, '/* The separator CSS */' ); -// } else { -// error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); -// } -// wp_style_add_data( $handle, 'path', $path ); - - $handle = 'wp-block-separator'; - $done = wp_styles()->done; - error_log( __FILE__ . ':' . __LINE__ . ": wp_print_styles($handle): " . get_echo( 'wp_print_styles', array( $handle ) ) ); - wp_styles()->done = $done; } $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); @@ -1754,9 +1714,6 @@ static function () { 'the_content', '
' ); - if ( ! empty( $GLOBALS['debug_on_demand_block_style'] ) ) { - error_log( __FILE__ . ':' . __LINE__ . ' wp_styles()->queue: ' . json_encode( wp_styles()->queue ) ); - } // Simulate footer scripts. $footer_output = get_echo( 'wp_footer' ); @@ -1802,9 +1759,6 @@ static function () { $found_subset_styles, 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_styles ) ); - - unset( $GLOBALS['debug_on_demand_block_style'] ); // TODO: Remove. - error_log( "\n##END DEBUG ####################################\n" ); // TODO: Remove. } /** @@ -1820,8 +1774,7 @@ static function () { */ private function ensure_style_asset_file_created( string $handle, string $relative_path ) { $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-separator style to be registered.' ); - $dependency = wp_styles()->query( $handle ); - error_log( __FILE__ . ':' . __LINE__ . ' $dependency->src === ' . $dependency->src ); + $dependency = wp_styles()->query( $handle ); $dependency->src = includes_url( $relative_path ); $path = ABSPATH . WPINC . '/' . $relative_path; if ( ! file_exists( $path ) ) { @@ -1829,10 +1782,7 @@ private function ensure_style_asset_file_created( string $handle, string $relati if ( ! file_exists( $dir ) ) { mkdir( $dir, 0777, true ); } - error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES NOT EXIST: ' . $path ); file_put_contents( $path, "/* CSS for $handle */" ); - } else { - error_log( __FILE__ . ':' . __LINE__ . ' FILE DOES EXIST: ' . $path ); } wp_style_add_data( $handle, 'path', $path ); } From 9607f0ef61b656f942736140328bf155a2947526 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 17:43:44 -0800 Subject: [PATCH 44/50] Opt for ignore list rather than ack list --- tests/phpunit/tests/template.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 99789ecb87527..6a0457fddb78c 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1747,17 +1747,24 @@ static function () { /* * Since new styles could appear at any time and since certain styles leak in from the global scope not being * properly reset somewhere else in the test suite, we only check that the expected styles are at least present - * and in the same order. + * and in the same order. When new styles are introduced in core, they may be added to this array as opposed to + * updating the arrays in the data provider, if appropriate. */ + $ignored_styles = array( + 'core-block-supports-duotone-inline-css', + 'wp-block-library-theme-css', + 'wp-block-template-skip-link-inline-css', + ); + $found_subset_styles = array(); foreach ( array( 'HEAD', 'BODY' ) as $group ) { - $found_subset_styles[ $group ] = array_values( array_intersect( $found_styles[ $group ], $expected_styles[ $group ] ) ); + $found_subset_styles[ $group ] = array_values( array_diff( $found_styles[ $group ], $ignored_styles ) ); } $this->assertSame( $expected_styles, $found_subset_styles, - 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_styles ) + 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_subset_styles ) ); } From 9b86cdecba4d16a65a0a96d95fb6a4fde32b6d03 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 18:52:35 -0800 Subject: [PATCH 45/50] Only use placeholder comment for ensuring wp-block-library inline style is printed and ensure it is added just in time --- src/wp-includes/script-loader.php | 26 ++++++++++++--------- tests/phpunit/tests/template.php | 39 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 26facbb8afe1a..0916562dda594 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3644,12 +3644,18 @@ function wp_hoist_late_printed_styles() { } /* - * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header - * by means of a filter below on the template enhancement output buffer. + * Add a placeholder comment into the inline styles for wp-block-library, after which where the late block styles + * can be hoisted from the footer to be printed in the header by means of a filter below on the template enhancement + * output buffer. The `wp_print_styles` action is used to ensure that if the inline style gets replaced at + * `enqueue_block_assets` or `wp_enqueue_scripts` that the placeholder will be sure to be present. */ - $placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) ); - - wp_add_inline_style( 'wp-block-library', $placeholder ); + $placeholder = sprintf( '/*%s*/', uniqid( 'wp_block_styles_on_demand_placeholder:' ) ); + add_action( + 'wp_print_styles', + static function () use ( $placeholder ) { + wp_add_inline_style( 'wp-block-library', $placeholder ); + } + ); /* * Create a substitute for `print_late_styles()` which is aware of block styles. This substitute does not print @@ -3785,15 +3791,13 @@ public function remove() { 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { - // If the inline style lacks the placeholder comment, then all the styles will be inserted below at . $css_text = $processor->get_modifiable_text(); - if ( ! str_contains( $css_text, $placeholder ) ) { - continue; - } /* - * Remove the placeholder now that we've located the inline style (and remove the inline style if it - * is now empty, aside from a sourceURL comment). + * A placeholder CSS comment is added to the inline style in order to force an inline STYLE tag to + * be printed. Now that we've located the inline style, the placeholder comment can be removed. If + * there is no CSS left in the STYLE tag after removing the placeholder (aside from the sourceURL + * comment, then remove the STYLE entirely.) */ $css_text = str_replace( $placeholder, '', $css_text ); if ( preg_match( ':^/\*# sourceURL=\S+? \*/$:', trim( $css_text ) ) ) { diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 6a0457fddb78c..da105f1c40cdd 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1632,6 +1632,38 @@ function (): void { ), ), ), + 'override_block_library_inline_style_late' => array( + 'set_up' => static function () { + add_action( + 'enqueue_block_assets', + function (): void { + // This tests what happens when the placeholder comment gets replaced unexpectedly. + wp_styles()->registered['wp-block-library']->extra['after'] = array( '/* OVERRIDDEN! */' ); + } + ); + }, + 'inline_size_limit' => 0, + 'expected_styles' => array( + 'HEAD' => array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + 'wp-block-library-css', + 'wp-block-library-inline-css', // This contains the "OVERRIDDEN" text. + 'wp-block-separator-css', + 'global-styles-inline-css', + 'core-block-supports-inline-css', + 'classic-theme-styles-css', + 'normal-css', + 'normal-inline-css', + 'wp-custom-css', + 'late-css', + 'late-inline-css', + ), + 'BODY' => array(), + ), + ), ); } @@ -1721,12 +1753,17 @@ 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' ) ) { + $this->assertMatchesRegularExpression( $placeholder_regexp, $buffer, 'Expected the placeholder to be present in the buffer.' ); + } + // Apply the output buffer filter. $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer ); $this->assertStringContainsString( '', $filtered_buffer, 'Expected the closing HEAD tag to be in the response.' ); - $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' ); + $this->assertDoesNotMatchRegularExpression( $placeholder_regexp, $filtered_buffer, 'Expected the placeholder to be removed.' ); $found_styles = array( 'HEAD' => array(), 'BODY' => array(), From 9f816a31ad61f2136780c9107f4a332056a6b599 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 18:53:39 -0800 Subject: [PATCH 46/50] Improve ensure_style_asset_file_created helper --- tests/phpunit/tests/template.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index da105f1c40cdd..74185b775ba5f 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1707,11 +1707,10 @@ static function () { $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' ); // Ensure stylesheet files exist on the filesystem since a build may not have been done. - $this->ensure_style_asset_file_created( 'wp-block-library', 'css/dist/block-library/style.css' ); + $this->ensure_style_asset_file_created( 'wp-block-library' ); if ( wp_should_load_separate_core_block_assets() ) { - $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' ); + $this->ensure_style_asset_file_created( 'wp-block-separator' ); } - $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); // Enqueue a style early, before wp_enqueue_scripts. @@ -1813,12 +1812,22 @@ static function () { * * self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); * - * @param string $handle Style handle. - * @param string $relative_path Relative path to the CSS file in the includes directory. + * @param string $handle Style handle. + * + * @throws Exception If the supplied style handle is not registered as expected. */ - private function ensure_style_asset_file_created( string $handle, string $relative_path ) { - $this->assertTrue( wp_style_is( $handle, 'registered' ), 'Expected the wp-block-separator style to be registered.' ); - $dependency = wp_styles()->query( $handle ); + private function ensure_style_asset_file_created( string $handle ) { + $dependency = wp_styles()->query( $handle ); + if ( ! $dependency ) { + throw new Exception( "The stylesheet for $handle is not registered." ); + } + if ( ! $dependency->src ) { + throw new Exception( "The stylesheet URL for $handle is empty." ); + } + if ( ! str_contains( $dependency->src, '/wp-includes/' ) ) { + throw new Exception( "Expected the stylesheet URL for $handle to be in the includes directory, but got {$dependency->src}" ); + } + $relative_path = preg_replace( '#^.*?/wp-includes/#', '/', $dependency->src ); $dependency->src = includes_url( $relative_path ); $path = ABSPATH . WPINC . '/' . $relative_path; if ( ! file_exists( $path ) ) { From 0a6564ef4050d63b8297f24d356864747849c74e Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Fri, 7 Nov 2025 14:21:15 +1100 Subject: [PATCH 47/50] Remove see wrappers of auto-linked function names in dev docs. --- src/wp-includes/script-loader.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 0916562dda594..202f10fafe4c4 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2264,14 +2264,14 @@ function wp_print_head_scripts() { /** * Private, for use in *_footer_scripts hooks * - * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()}, - * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the printing of + * In classic themes, when block styles are loaded on demand via wp_load_classic_theme_block_styles_on_demand(), + * this function is replaced by a closure in wp_hoist_late_printed_styles() which will capture the printing of * two sets of "late" styles to be hoisted to the HEAD by means of the template enhancement output buffer: * * 1. Styles related to blocks are inserted right after the wp-block-library stylesheet. * 2. All other styles are appended to the end of the HEAD. * - * The closure calls {@see print_footer_scripts()} to print scripts in the footer as usual. + * The closure calls print_footer_scripts() to print scripts in the footer as usual. * * @since 3.3.0 */ From 885bdddd720f45ad13c920f83f1a7a19a3c7acca Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 19:32:51 -0800 Subject: [PATCH 48/50] Add missing static to closure --- 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 202f10fafe4c4..370b341ddd21b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3732,7 +3732,7 @@ static function () use ( $capture_late_styles ) { // Replace placeholder with the captured late styles. add_filter( 'wp_template_enhancement_output_buffer', - function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) { + static function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) { // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { From 2216f141253b2d9a99a7dd47d1ec5b0d44ba1736 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 19:36:19 -0800 Subject: [PATCH 49/50] Unimprove ensure_style_asset_file_created helper Reverts part of 9f816a31ad61f2136780c9107f4a332056a6b599 It turns out the depenency src is empty when a build hasn't been done --- tests/phpunit/tests/template.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 74185b775ba5f..a304fff95f865 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1707,9 +1707,12 @@ static function () { $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' ); // Ensure stylesheet files exist on the filesystem since a build may not have been done. - $this->ensure_style_asset_file_created( 'wp-block-library' ); + $this->ensure_style_asset_file_created( + 'wp-block-library', + wp_should_load_separate_core_block_assets() ? 'css/dist/block-library/common.css' : 'css/dist/block-library/style.css' + ); if ( wp_should_load_separate_core_block_assets() ) { - $this->ensure_style_asset_file_created( 'wp-block-separator' ); + $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' ); } $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' ); @@ -1812,22 +1815,16 @@ static function () { * * self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); * - * @param string $handle Style handle. + * @param string $handle Style handle. + * @param string $relative_path Relative path to the CSS file in wp-includes. * * @throws Exception If the supplied style handle is not registered as expected. */ - private function ensure_style_asset_file_created( string $handle ) { + private function ensure_style_asset_file_created( string $handle, string $relative_path ) { $dependency = wp_styles()->query( $handle ); if ( ! $dependency ) { throw new Exception( "The stylesheet for $handle is not registered." ); } - if ( ! $dependency->src ) { - throw new Exception( "The stylesheet URL for $handle is empty." ); - } - if ( ! str_contains( $dependency->src, '/wp-includes/' ) ) { - throw new Exception( "Expected the stylesheet URL for $handle to be in the includes directory, but got {$dependency->src}" ); - } - $relative_path = preg_replace( '#^.*?/wp-includes/#', '/', $dependency->src ); $dependency->src = includes_url( $relative_path ); $path = ABSPATH . WPINC . '/' . $relative_path; if ( ! file_exists( $path ) ) { From 732b28eb1e7bd99935d3a3b1939039a2bdc091ba Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Nov 2025 19:41:16 -0800 Subject: [PATCH 50/50] Update phpcs exclusion after 885bdddd720f45ad13c920f83f1a7a19a3c7acca --- phpcompat.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpcompat.xml.dist b/phpcompat.xml.dist index daf3cdbe52c93..a4771be79618b 100644 --- a/phpcompat.xml.dist +++ b/phpcompat.xml.dist @@ -118,7 +118,7 @@ Excluded while waiting for PHPCompatibility v10. See . --> - + /src/wp-includes/script-loader\.php$