@@ -2264,6 +2264,11 @@ function wp_print_head_scripts() {
22642264/**
22652265 * Private, for use in *_footer_scripts hooks
22662266 *
2267+ * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()},
2268+ * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of
2269+ * {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted
2270+ * to the HEAD by means of the template enhancement output buffer.
2271+ *
22672272 * @since 3.3.0
22682273 */
22692274function _wp_footer_scripts () {
@@ -3232,6 +3237,7 @@ static function () use ( $style ) {
32323237 * }
32333238 */
32343239function wp_enqueue_stored_styles ( $ options = array () ) {
3240+ // Note: Styles printed at wp_footer for classic themes may still end up in the head due to wp_load_classic_theme_block_styles_on_demand().
32353241 $ is_block_theme = wp_is_block_theme ();
32363242 $ is_classic_theme = ! $ is_block_theme ;
32373243
@@ -3469,6 +3475,153 @@ function wp_remove_surrounding_empty_script_tags( $contents ) {
34693475 }
34703476}
34713477
3478+ /**
3479+ * Adds hooks to load block styles on demand in classic themes.
3480+ *
3481+ * @since 6.9.0
3482+ */
3483+ function wp_load_classic_theme_block_styles_on_demand () {
3484+ if ( wp_is_block_theme () ) {
3485+ return ;
3486+ }
3487+
3488+ /*
3489+ * Make sure that wp_should_output_buffer_template_for_enhancement() returns true even if there aren't any
3490+ * `wp_template_enhancement_output_buffer` filters added, but do so at priority zero so that applications which
3491+ * wish to stream responses can more easily turn this off.
3492+ */
3493+ add_filter ( 'wp_should_output_buffer_template_for_enhancement ' , '__return_true ' , 0 );
3494+
3495+ if ( ! wp_should_output_buffer_template_for_enhancement () ) {
3496+ return ;
3497+ }
3498+
3499+ /*
3500+ * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally,
3501+ * and so that block-specific styles will only be enqueued when they are used on the page.
3502+ */
3503+ add_filter ( 'should_load_separate_core_block_assets ' , '__return_true ' , 0 );
3504+
3505+ // Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
3506+ add_filter ( 'should_load_block_assets_on_demand ' , '__return_true ' , 0 );
3507+
3508+ // Add hooks which require the presence of the output buffer. Ideally the above two filters could be added here, but they run too early.
3509+ add_action ( 'wp_template_enhancement_output_buffer_started ' , 'wp_hoist_late_printed_styles ' );
3510+ }
3511+
3512+ /**
3513+ * Adds the hooks needed for CSS output to be delayed until after the content of the page has been established.
3514+ *
3515+ * @since 6.9.0
3516+ *
3517+ * @see wp_load_classic_theme_block_styles_on_demand()
3518+ * @see _wp_footer_scripts()
3519+ */
3520+ function wp_hoist_late_printed_styles () {
3521+ // Skip the embed template on-demand styles aren't relevant, and there is no wp_head action.
3522+ if ( is_embed () ) {
3523+ return ;
3524+ }
3525+
3526+ /*
3527+ * While normally late styles are printed, there is a filter to disable prevent this, so this makes sure they are
3528+ * printed. Note that this filter was intended to control whether to print the styles queued too late for the HTML
3529+ * head. This filter was introduced in <https://core.trac.wordpress.org/ticket/9346>. However, with the template
3530+ * enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can
3531+ * always hoist it to the HEAD.
3532+ */
3533+ add_filter ( 'print_late_styles ' , '__return_true ' , PHP_INT_MAX );
3534+
3535+ /*
3536+ * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header
3537+ * by means of a filter below on the template enhancement output buffer.
3538+ */
3539+ $ placeholder = sprintf ( '/*%s*/ ' , uniqid ( 'wp_late_styles_placeholder: ' ) );
3540+
3541+ wp_add_inline_style ( 'wp-block-library ' , $ placeholder );
3542+
3543+ // Wrap print_late_styles() with a closure that captures the late-printed styles.
3544+ $ printed_late_styles = '' ;
3545+ $ capture_late_styles = static function () use ( &$ printed_late_styles ) {
3546+ ob_start ();
3547+ print_late_styles ();
3548+ $ printed_late_styles = ob_get_clean ();
3549+ };
3550+
3551+ /*
3552+ * If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts()
3553+ * was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the
3554+ * late-printed styles.
3555+ *
3556+ * Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then
3557+ * swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before
3558+ * proceeding with printing the footer scripts.
3559+ */
3560+ $ wp_print_footer_scripts_priority = has_action ( 'wp_print_footer_scripts ' , '_wp_footer_scripts ' );
3561+ if ( false === $ wp_print_footer_scripts_priority || false === has_action ( 'wp_footer ' , 'wp_print_footer_scripts ' ) ) {
3562+ // The normal priority for wp_print_footer_scripts() is to run at 20.
3563+ add_action ( 'wp_footer ' , $ capture_late_styles , 20 );
3564+ } else {
3565+ remove_action ( 'wp_print_footer_scripts ' , '_wp_footer_scripts ' , $ wp_print_footer_scripts_priority );
3566+ add_action (
3567+ 'wp_print_footer_scripts ' ,
3568+ static function () use ( $ capture_late_styles ) {
3569+ $ capture_late_styles ();
3570+ print_footer_scripts ();
3571+ },
3572+ $ wp_print_footer_scripts_priority
3573+ );
3574+ }
3575+
3576+ // Replace placeholder with the captured late styles.
3577+ add_filter (
3578+ 'wp_template_enhancement_output_buffer ' ,
3579+ function ( $ buffer ) use ( $ placeholder , &$ printed_late_styles ) {
3580+
3581+ // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
3582+ $ processor = new class ( $ buffer ) extends WP_HTML_Tag_Processor {
3583+ public function get_span (): WP_HTML_Span {
3584+ $ instance = $ this ; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class.
3585+ $ instance ->set_bookmark ( 'here ' );
3586+ return $ instance ->bookmarks ['here ' ];
3587+ }
3588+ };
3589+
3590+ // Loop over STYLE tags.
3591+ while ( $ processor ->next_tag ( array ( 'tag_name ' => 'STYLE ' ) ) ) {
3592+ // Skip to the next if this is not the inline style for the wp-block-library stylesheet (which contains the placeholder).
3593+ if ( 'wp-block-library-inline-css ' !== $ processor ->get_attribute ( 'id ' ) ) {
3594+ continue ;
3595+ }
3596+
3597+ // If the inline style lacks the placeholder comment, then something went wrong and we need to abort.
3598+ $ css_text = $ processor ->get_modifiable_text ();
3599+ if ( ! str_contains ( $ css_text , $ placeholder ) ) {
3600+ break ;
3601+ }
3602+
3603+ // Remove the placeholder now that we've located the inline style.
3604+ $ processor ->set_modifiable_text ( str_replace ( $ placeholder , '' , $ css_text ) );
3605+ $ buffer = $ processor ->get_updated_html ();
3606+
3607+ // Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
3608+ $ span = $ processor ->get_span ();
3609+ $ buffer = implode (
3610+ '' ,
3611+ array (
3612+ substr ( $ buffer , 0 , $ span ->start + $ span ->length ),
3613+ $ printed_late_styles ,
3614+ substr ( $ buffer , $ span ->start + $ span ->length ),
3615+ )
3616+ );
3617+ break ;
3618+ }
3619+
3620+ return $ buffer ;
3621+ }
3622+ );
3623+ }
3624+
34723625/**
34733626 * Return the corresponding JavaScript `dataset` name for an attribute
34743627 * if it represents a custom data attribute, or `null` if not.
0 commit comments