<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[61174] trunk: Script Loader: Improve hoisted stylesheet ordering (in classic themes) to preserve CSS cascade.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/61174">61174</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/61174","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2025-11-07 04:27:45 +0000 (Fri, 07 Nov 2025)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Script Loader: Improve hoisted stylesheet ordering (in classic themes) to preserve CSS cascade.

This ensures that on-demand block styles are inserted right after the `wp-block-library` inline style whereas other stylesheets not related to blocks are appended to the end of the `HEAD`. This helps ensure the expected cascade is preserved. If no `wp-block-library` inline style is present, then all styles get appended to the `HEAD` regardless.

The handling of the CSS placeholder comment added to the `wp-block-library` inline style is also improved. It is now inserted later to ensure the inline style is printed. Additionally, when the CSS placeholder comment is removed from the `wp-block-library` inline style, the entire `STYLE` tag is now removed if there are no styles left (aside from the `sourceURL` comment).

Lastly, the use of the HTML Tag Processor is significantly improved to leverage `WP_HTML_Text_Replacement`.

Developed in https://github.com/WordPress/wordpress-develop/pull/10436

Follow-up to <a href="https://core.trac.wordpress.org/changeset/61008">[61008]</a>.

Props westonruter, peterwilsoncc, dmsnell.
Fixes <a href="https://core.trac.wordpress.org/ticket/64099">#64099</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkphpcompatxmldist">trunk/phpcompat.xml.dist</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunktestsphpunitteststemplatephp">trunk/tests/phpunit/tests/template.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkphpcompatxmldist"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/phpcompat.xml.dist</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/phpcompat.xml.dist  2025-11-07 02:59:09 UTC (rev 61173)
+++ trunk/phpcompat.xml.dist    2025-11-07 04:27:45 UTC (rev 61174)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -114,4 +114,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <exclude-pattern>/sodium_compat/src/PHP52/SplFixedArray\.php$</exclude-pattern>
</span><span class="cx" style="display: block; padding: 0 10px">        </rule>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        <!--
+               Excluded while waiting for PHPCompatibility v10.
+               See <https://github.com/PHPCompatibility/PHPCompatibility/issues/1481>.
+       -->
+       <rule ref="PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic">
+               <exclude-pattern>/src/wp-includes/script-loader\.php$</exclude-pattern>
+       </rule>
+
</ins><span class="cx" style="display: block; padding: 0 10px"> </ruleset>
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2025-11-07 02:59:09 UTC (rev 61173)
+++ trunk/src/wp-includes/script-loader.php     2025-11-07 04:27:45 UTC (rev 61174)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2264,11 +2264,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Private, for use in *_footer_scripts hooks
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * 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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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:
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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 print_footer_scripts() to print scripts in the footer as usual.
+ *
</ins><span class="cx" style="display: block; padding: 0 10px">  * @since 3.3.0
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function _wp_footer_scripts() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3601,20 +3605,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">        // The following two filters are added by default for block themes in _add_default_theme_supports().
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * 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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( ! wp_should_load_separate_core_block_assets() ) {
+               return;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /*
</span><span class="cx" style="display: block; padding: 0 10px">         * Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-       // 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() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! wp_should_load_block_assets_on_demand() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3637,37 +3644,73 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * 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 <https://core.trac.wordpress.org/ticket/9346>. 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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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 );
+               }
+       );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * 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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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 ) {
+                       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',
+                               'block-style-variation-styles',
+                               'core-block-supports',
+                               'core-block-supports-duotone',
+                       )
+               );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        wp_add_inline_style( 'wp-block-library', $placeholder );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * 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 ) {
+                       ob_start();
+                       wp_styles()->do_items( $enqueued_block_styles );
+                       $printed_block_styles = ob_get_clean();
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // 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 ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * 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).
+                */
</ins><span class="cx" style="display: block; padding: 0 10px">                 ob_start();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                print_late_styles();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_styles()->do_footer_items();
</ins><span class="cx" style="display: block; padding: 0 10px">                 $printed_late_styles = ob_get_clean();
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * 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
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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
</ins><span class="cx" style="display: block; padding: 0 10px">          * late-printed styles.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * 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
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * 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
</ins><span class="cx" style="display: block; padding: 0 10px">          * proceeding with printing the footer scripts.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3689,65 +3732,99 @@
</span><span class="cx" style="display: block; padding: 0 10px">        // Replace placeholder with the captured late styles.
</span><span class="cx" style="display: block; padding: 0 10px">        add_filter(
</span><span class="cx" style="display: block; padding: 0 10px">                'wp_template_enhancement_output_buffer',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                function ( $buffer ) use ( $placeholder, &$printed_late_styles ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         static function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
</span><span class="cx" style="display: block; padding: 0 10px">                        $processor = new class( $buffer ) extends WP_HTML_Tag_Processor {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                public function get_span(): WP_HTML_Span {
-                                       $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class.
-                                       $instance->set_bookmark( 'here' );
-                                       return $instance->bookmarks['here'];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         /**
+                                * Gets the span for the current token.
+                                *
+                                * @return WP_HTML_Span Current token span.
+                                */
+                               private function get_span(): WP_HTML_Span {
+                                       // 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'];
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               /**
+                                * 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 );
+                               }
+
+                               /**
+                                * Removes the current token.
+                                */
+                               public function remove() {
+                                       $span = $this->get_span();
+
+                                       $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                         };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // Loop over STYLE tags.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles
+                        * at </head> (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.
+                        */
</ins><span class="cx" style="display: block; padding: 0 10px">                         while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-                               // We've encountered the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment.
</del><span class="cx" style="display: block; padding: 0 10px">                                 if (
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        ! $processor->is_tag_closer() &&
</del><span class="cx" style="display: block; padding: 0 10px">                                         'STYLE' === $processor->get_tag() &&
</span><span class="cx" style="display: block; padding: 0 10px">                                        'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
</span><span class="cx" style="display: block; padding: 0 10px">                                ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        // If the inline style lacks the placeholder comment, then we have to continue until we get to </HEAD> to append the styles there.
</del><span class="cx" style="display: block; padding: 0 10px">                                         $css_text = $processor->get_modifiable_text();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( ! str_contains( $css_text, $placeholder ) ) {
-                                               continue;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                       /*
+                                        * 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 ) ) ) {
+                                               $processor->remove();
+                                       } else {
+                                               $processor->set_modifiable_text( $css_text );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        // 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();
-
</del><span class="cx" style="display: block; padding: 0 10px">                                         // Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $span   = $processor->get_span();
-                                       $buffer = implode(
-                                               '',
-                                               array(
-                                                       substr( $buffer, 0, $span->start + $span->length ),
-                                                       $printed_late_styles,
-                                                       substr( $buffer, $span->start + $span->length ),
-                                               )
-                                       );
-                                       break;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( '' !== $printed_block_styles ) {
+                                               $processor->insert_after( $printed_block_styles );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // 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 ),
-                                               )
-                                       );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         // Prevent printing them again at </head>.
+                                               $printed_block_styles = '';
+                                       }
+
+                                       // If there aren't any late styles, there's no need to continue to finding </head>.
+                                       if ( '' === $printed_late_styles ) {
+                                               break;
+                                       }
+                               } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) {
+                                       $processor->insert_before( $printed_block_styles . $printed_late_styles );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         break;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return $buffer;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return $processor->get_updated_html();
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunitteststemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/template.php    2025-11-07 02:59:09 UTC (rev 61173)
+++ trunk/tests/phpunit/tests/template.php      2025-11-07 04:27:45 UTC (rev 61174)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -127,10 +127,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->original_wp_styles  = $wp_styles;
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_scripts                = null;
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_styles                 = null;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_scripts();
-               wp_styles();
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->original_theme_features = $GLOBALS['_wp_theme_features'];
</del><span class="cx" style="display: block; padding: 0 10px">                 foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->original_ini_config[ $option ] = ini_get( $option );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -141,7 +138,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_scripts = $this->original_wp_scripts;
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_styles  = $this->original_wp_styles;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $GLOBALS['_wp_theme_features'] = $this->original_theme_features;
</del><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $this->original_ini_config as $option => $value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        ini_set( $option, $value );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -149,6 +145,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                unregister_post_type( 'cpt' );
</span><span class="cx" style="display: block; padding: 0 10px">                unregister_taxonomy( 'taxo' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->set_permalink_structure( '' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 parent::tear_down();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -630,7 +627,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter(
</span><span class="cx" style="display: block; padding: 0 10px">                        'wp_template_enhancement_output_buffer',
</span><span class="cx" style="display: block; padding: 0 10px">                        static function () {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                return '<html>Hey!</html>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         return '<html lang="en"><head><meta charset="utf-8"></head><body>Hey!</body></html>';
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">                $level = ob_get_level();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1422,7 +1419,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        add_filter( 'should_load_separate_core_block_assets', '__return_false' );
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><span class="cx" style="display: block; padding: 0 10px">                                'expected_load_separate'  => false,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'expected_on_demand'      => true,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'expected_on_demand'      => false,
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'expected_buffer_started' => false,
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'classic_theme_with_should_load_block_assets_on_demand_out_out' => array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1475,34 +1472,198 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Data provider.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return array<string, array{set_up: Closure|null}>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return array<string, array{set_up: Closure|null, inline_size_limit: int,  expected_styles: array{ HEAD: string[], BODY: string[] }}>
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function data_wp_hoist_late_printed_styles(): array {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $common_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-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',
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'no_actions_removed'              => array(
-                               'set_up' => null,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 '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(),
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        '_wp_footer_scripts_removed'      => array(
-                               'set_up' => static function () {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 '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 () {
+                                       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. */' );
+                                               }
+                                       );
+                               },
+                               'inline_size_limit' => 0,
+                               'expected_styles'   => array(
+                                       'HEAD' => ( function ( $expected_styles ) {
+                                               // Insert 'wp-block-library-inline-css' right after 'wp-block-library-css'.
+                                               $i = array_search( 'wp-block-library-css', $expected_styles, true );
+                                               $this->assertIsInt( $i, 'Expected wp-block-library-css to be among the styles.' );
+                                               array_splice( $expected_styles, $i + 1, 0, 'wp-block-library-inline-css' );
+                                               return $expected_styles;
+                                       } )( $common_expected_head_styles ),
+                                       'BODY' => array(),
+                               ),
+                       ),
+                       'classic_theme_opt_out_separate_block_styles' => array(
+                               'set_up'            => static function () {
+                                       add_filter( 'should_load_separate_core_block_assets', '__return_false' );
+                               },
+                               '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',
+                                               '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_footer_scripts_removed'                  => array(
+                               'set_up'            => static function () {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'inline_size_limit' => 0,
+                               'expected_styles'   => array(
+                                       'HEAD' => $common_expected_head_styles,
+                                       'BODY' => array(),
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'wp_print_footer_scripts_removed' => array(
-                               'set_up' => static function () {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'wp_print_footer_scripts_removed'             => array(
+                               'set_up'            => static function () {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'inline_size_limit' => 0,
+                               'expected_styles'   => array(
+                                       'HEAD' => $common_expected_head_styles,
+                                       'BODY' => array(),
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'both_actions_removed'            => array(
-                               'set_up' => static function () {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'both_actions_removed'                        => array(
+                               'set_up'            => static function () {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
</span><span class="cx" style="display: block; padding: 0 10px">                                        remove_action( 'wp_footer', 'wp_print_footer_scripts' );
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'inline_size_limit' => 0,
+                               'expected_styles'   => array(
+                                       'HEAD' => $common_expected_head_styles,
+                                       'BODY' => array(),
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'block_library_removed'           => array(
-                               'set_up' => static function () {
-                                       wp_deregister_style( 'wp-block-library' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 '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' );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                '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',
+                                               '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',
+                                       ),
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '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(),
+                               ),
+                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1510,26 +1671,68 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * 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.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 64099
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @covers ::wp_load_classic_theme_block_styles_on_demand
</ins><span class="cx" style="display: block; padding: 0 10px">          * @covers ::wp_hoist_late_printed_styles
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @dataProvider data_wp_hoist_late_printed_styles
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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;
+
+               // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots.
+               add_filter(
+                       'styles_inline_size_limit',
+                       static function () use ( $inline_size_limit ): int {
+                               return $inline_size_limit;
+                       }
+               );
+
+               add_filter(
+                       'wp_get_custom_css',
+                       static function () {
+                               return '/* CUSTOM CSS from Customizer */';
+                       }
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $set_up ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $set_up();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                switch_theme( 'default' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_load_classic_theme_block_styles_on_demand();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Enqueue a style
-               wp_enqueue_style( 'early', 'http://example.com/style.css' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // 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 stylesheet files exist on the filesystem since a build may not have been done.
+               $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', 'blocks/separator/style.css' );
+               }
+               $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' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_add_inline_style( 'early', '/* EARLY */' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_hoist_late_printed_styles();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // 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 */' );
+                       }
+               );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // 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.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // 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();
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Simulate wp_head.
</span><span class="cx" style="display: block; padding: 0 10px">                $head_output = get_echo( 'wp_head' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1537,21 +1740,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertStringContainsString( 'early', $head_output, 'Expected the early-enqueued stylesheet to be present.' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Enqueue a late style (after wp_head).
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_enqueue_style( 'late', 'http://example.com/late-style.css', array(), null );
-               wp_add_inline_style( 'late', '/* EARLY */' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null );
+               wp_add_inline_style( 'late', '/* LATE */' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Simulate the_content().
+               $content = apply_filters(
+                       'the_content',
+                       '<!-- wp:separator --><hr class="wp-block-separator has-alpha-channel-opacity"/><!-- /wp:separator -->'
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Simulate footer scripts.
</span><span class="cx" style="display: block; padding: 0 10px">                $footer_output = get_echo( 'wp_footer' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Create a simulated output buffer.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $buffer = '<html><head>' . $head_output . '</head><body><main>Content</main>' . $footer_output . '</body></html>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $buffer = '<html lang="en"><head><meta charset="utf-8">' . $head_output . '</head><body><main>' . $content . '</main>' . $footer_output . '</body></html>';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $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.' );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Apply the output buffer filter.
</span><span class="cx" style="display: block; padding: 0 10px">                $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertStringContainsString( '</head>', $buffer, 'Expected the closing HEAD tag to be in the response.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertStringContainsString( '</head>', $filtered_buffer, 'Expected the closing HEAD tag to be in the response.' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9-]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertDoesNotMatchRegularExpression( $placeholder_regexp, $filtered_buffer, 'Expected the placeholder to be removed.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $found_styles = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'HEAD' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'BODY' => array(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1569,23 +1783,60 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $expected = array(
-                       'early-css',
-                       'early-inline-css',
-                       'late-css',
-                       'late-inline-css',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * 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. 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',
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( $expected as $style_id ) {
-                       $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $found_subset_styles = array();
+               foreach ( array( 'HEAD', 'BODY' ) as $group ) {
+                       $found_subset_styles[ $group ] = array_values( array_diff( $found_styles[ $group ], $ignored_styles ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertSame(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $expected,
-                       array_values( array_intersect( $found_styles['HEAD'], $expected ) ),
-                       'Expected styles to be printed in the same order.'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $expected_styles,
+                       $found_subset_styles,
+                       'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_subset_styles )
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' );
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * 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 wp-includes.
+        *
+        * @throws Exception If the supplied style handle is not registered as expected.
+        */
+       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." );
+               }
+               $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 );
+                       }
+                       file_put_contents( $path, "/* CSS for $handle */" );
+               }
+               wp_style_add_data( $handle, 'path', $path );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public function assertTemplateHierarchy( $url, array $expected, $message = '' ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->go_to( $url );
</span><span class="cx" style="display: block; padding: 0 10px">                $hierarchy = $this->get_template_hierarchy();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1593,6 +1844,49 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( $expected, $hierarchy, $message );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * 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',
+        *      )
+        *
+        *
+        * @param array $snapshot Snapshot.
+        * @return string Snapshot export.
+        */
+       private static function get_array_snapshot_export( array $snapshot ): string {
+               $export = var_export( $snapshot, true );
+               $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 ) {
+                               return str_repeat( "\t", strlen( $matches[0] ) / 2 );
+                       },
+                       $export
+               );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         protected static function get_query_template_conditions() {
</span><span class="cx" style="display: block; padding: 0 10px">                return array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'embed'             => 'is_embed',
</span></span></pre>
</div>
</div>

</body>
</html>