Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
137e732
Use well-formed HTML in tests
westonruter Oct 28, 2025
f5d49cd
Use HTTPS and fix late inline style contents
westonruter Oct 28, 2025
cccb1ef
Add separate test case specifically for filtering print_late_styles
westonruter Oct 28, 2025
137b9de
Add comment for PHPCompatibility issue
westonruter Oct 28, 2025
8975233
Use more specific return type for WP_Block_Type_Registry::get_all_reg…
westonruter Oct 29, 2025
6308437
Remove PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOuts…
westonruter Oct 29, 2025
2d94ec6
Add a rendered block to the output
westonruter Oct 29, 2025
2d537f7
Add failing test cases for test_wp_hoist_late_printed_styles
westonruter Oct 29, 2025
92cd33c
Exclude PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOut…
westonruter Oct 30, 2025
d5a964b
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 2, 2025
53d21e6
Print block styles after wp-block-library and everything else at end …
westonruter Nov 2, 2025
9d8bd27
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 5, 2025
156c6f5
Add example for get_array_snapshot_export
westonruter Nov 5, 2025
16d3f54
Prevent enabling should_load_block_assets_on_demand if should_load_se…
westonruter Nov 5, 2025
99b1b42
Improve tests
westonruter Nov 5, 2025
28faf6d
Remove obsolete print_late_styles override
westonruter Nov 5, 2025
d305804
Update tests
westonruter Nov 5, 2025
e8482a0
Add todo comments
westonruter Nov 5, 2025
54b2474
Refactor HTML Tag Processor logic
westonruter Nov 5, 2025
d93e767
Fix missing use of footer in assertion
westonruter Nov 5, 2025
ec25a5f
Use correct variable in assertion
westonruter Nov 5, 2025
53ef0c6
Remove unhelpful test cases since print_late_styles filter seems to h…
westonruter Nov 5, 2025
ead1aa2
Set styles_inline_size_limit to unlimited for test
westonruter Nov 5, 2025
d2afb80
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 5, 2025
27f5e33
Opt to set styles_inline_size_limit to zero for test
westonruter Nov 5, 2025
3c77325
Test that non-empty wp-block-library inline style is not removed
westonruter Nov 5, 2025
12d4ae7
Consolidate assertions
westonruter Nov 5, 2025
1e5797c
Fix classic_theme_with_should_load_separate_core_block_assets_opt_out…
westonruter Nov 5, 2025
08f4809
Empty out added theme supports before running tests
westonruter Nov 5, 2025
a9a54ca
Add core-block-supports-duotone to the list of styles to print after …
westonruter Nov 5, 2025
c7a4c64
Improve formatting of empty array in export
westonruter Nov 5, 2025
a8fe022
Restore original block type registry after test
westonruter Nov 5, 2025
0ad3084
Fix tests when run as part of enture test suite
westonruter Nov 5, 2025
e3e5b3b
Revert unrelated WP_Block_Type_Registry change
westonruter Nov 5, 2025
9cc527e
Restore original_ini_config
westonruter Nov 5, 2025
dac85df
Remove admin-specific style printing logic
westonruter Nov 5, 2025
0d65fca
Carify comments
westonruter Nov 5, 2025
5d22771
Add test for when styles_inline_size_limit is unlimited
westonruter Nov 5, 2025
56c35c8
Remove obsolete wp_block_styles_not_supported test
westonruter Nov 5, 2025
73c5b07
Remove test for test which is not worthwhile
westonruter Nov 5, 2025
2fed203
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 5, 2025
18691a4
Replace todo with explanation of why set_bookmark will never return f…
westonruter Nov 5, 2025
3f0ac52
Restore comment to _wp_footer_scripts()
westonruter Nov 5, 2025
34d7573
Restore and update comment for _wp_footer_scripts()
westonruter Nov 5, 2025
2524386
Add assertion to ensure wp-block-separator style is registered
westonruter Nov 6, 2025
fd0c402
Debug
westonruter Nov 6, 2025
c2fd29f
Undebug
westonruter Nov 6, 2025
4447f46
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 7, 2025
9607f0e
Opt for ignore list rather than ack list
westonruter Nov 7, 2025
9b86cde
Only use placeholder comment for ensuring wp-block-library inline sty…
westonruter Nov 7, 2025
9f816a3
Improve ensure_style_asset_file_created helper
westonruter Nov 7, 2025
b8f8d37
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Nov 7, 2025
0a6564e
Remove see wrappers of auto-linked function names in dev docs.
peterwilsoncc Nov 7, 2025
885bddd
Add missing static to closure
westonruter Nov 7, 2025
2216f14
Unimprove ensure_style_asset_file_created helper
westonruter Nov 7, 2025
732b28e
Update phpcs exclusion after 885bdddd720f45ad13c920f83f1a7a19a3c7acca
westonruter Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions phpcompat.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,12 @@
<exclude-pattern>/sodium_compat/src/PHP52/SplFixedArray\.php$</exclude-pattern>
</rule>

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

</ruleset>
213 changes: 145 additions & 68 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2264,10 +2264,14 @@ function wp_print_head_scripts() {
/**
* Private, for use in *_footer_scripts hooks
*
* In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()},
* this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the 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.
* In classic themes, when block styles are loaded on demand via wp_load_classic_theme_block_styles_on_demand(),
* this function is replaced by a closure in wp_hoist_late_printed_styles() which will capture the printing of
* two sets of "late" styles to be hoisted to the HEAD by means of the template enhancement output buffer:
*
* 1. Styles related to blocks are inserted right after the wp-block-library stylesheet.
* 2. All other styles are appended to the end of the HEAD.
*
* The closure calls print_footer_scripts() to print scripts in the footer as usual.
*
* @since 3.3.0
*/
Expand Down Expand Up @@ -3601,20 +3605,23 @@ function wp_load_classic_theme_block_styles_on_demand() {
// The following two filters are added by default for block themes in _add_default_theme_supports().

/*
* Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally,
* and so that block-specific styles will only be enqueued when they are used on the page.
* A priority of zero allows for this to be easily overridden by themes which wish to opt out.
* Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, and so
* that block-specific styles will only be enqueued when they are used on the page. A priority of zero allows for
* this to be easily overridden by themes which wish to opt out. If a site has explicitly opted out of loading
* separate block styles, then abort.
*/
add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
if ( ! wp_should_load_separate_core_block_assets() ) {
return;
}

/*
* Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
* As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out.
* As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. If a site
* has explicitly opted out of loading block styles on demand, then abort.
*/
add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );

// If a site has explicitly opted out of loading block styles on demand via filters with priorities higher than above, then abort.
if ( ! wp_should_load_separate_core_block_assets() || ! wp_should_load_block_assets_on_demand() ) {
if ( ! wp_should_load_block_assets_on_demand() ) {
return;
}

Expand All @@ -3637,37 +3644,73 @@ function wp_hoist_late_printed_styles() {
}

/*
* 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.
* 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.
*/
add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX );
$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 );
}
);

/*
* 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.
* 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.
*/
$placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) );
$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',
)
);

wp_add_inline_style( 'wp-block-library', $placeholder );
/*
* 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();
}

// 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 ) {
/*
* 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).
*/
ob_start();
print_late_styles();
wp_styles()->do_footer_items();
$printed_late_styles = ob_get_clean();
};

/*
* If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts()
* was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the
* If `_wp_footer_scripts()` was unhooked from the `wp_print_footer_scripts` action, or if `wp_print_footer_scripts()`
* was unhooked from running at the `wp_footer` action, then only add a callback to `wp_footer` which will capture the
* late-printed styles.
*
* Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then
* swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before
* Otherwise, in the normal case where `_wp_footer_scripts()` will run at the `wp_print_footer_scripts` action, then
* swap out `_wp_footer_scripts()` with an alternative which captures the printed styles (for hoisting to HEAD) before
* proceeding with printing the footer scripts.
*/
$wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
Expand All @@ -3689,65 +3732,99 @@ static function () use ( $capture_late_styles ) {
// Replace placeholder with the captured late styles.
add_filter(
'wp_template_enhancement_output_buffer',
function ( $buffer ) use ( $placeholder, &$printed_late_styles ) {
static function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) {

// Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
$processor = new class( $buffer ) extends WP_HTML_Tag_Processor {
Copy link
Member Author

Choose a reason for hiding this comment

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

@dmsnell How does this updated WP_HTML_Tag_Processor extension look here? Note the insert_before and insert_after methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

And now a review method as well.

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'];
/**
* 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' );
Copy link
Member

Choose a reason for hiding this comment

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

technically it could also fail if there are too many bookmarks already. probably not an issue. in my own code I haven’t been documenting these cases of overlooking the failure given I control the whole flow of execution in the anonymous classes.

return $this->bookmarks['here'];
Copy link
Member

Choose a reason for hiding this comment

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

so much better with the removal of the additional surprising complexity!

}

/**
* 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, '' );
}
};

// Loop over STYLE tags.
/*
* 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.
*/
while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {

// We've encountered the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment.
if (
! $processor->is_tag_closer() &&
'STYLE' === $processor->get_tag() &&
'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
) {
// If the inline style lacks the placeholder comment, then we have to continue until we get to </HEAD> to append the styles there.
$css_text = $processor->get_modifiable_text();
if ( ! str_contains( $css_text, $placeholder ) ) {
continue;
}

// Remove the placeholder now that we've located the inline style.
$processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) );
$buffer = $processor->get_updated_html();
/*
* 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 );
}

// Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
$span = $processor->get_span();
$buffer = implode(
'',
array(
substr( $buffer, 0, $span->start + $span->length ),
$printed_late_styles,
substr( $buffer, $span->start + $span->length ),
)
);
break;
}
if ( '' !== $printed_block_styles ) {
$processor->insert_after( $printed_block_styles );

// 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 ),
)
);
// 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 );
break;
}
}

return $buffer;
return $processor->get_updated_html();
}
);
}
Expand Down
Loading
Loading