<!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>