Make WordPress Core


Ignore:
Timestamp:
11/07/2025 04:27:45 AM (3 months ago)
Author:
westonruter
Message:

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 [61008].

Props westonruter, peterwilsoncc, dmsnell.
Fixes #64099.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/script-loader.php

    r61142 r61174  
    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.
     2267 * In classic themes, when block styles are loaded on demand via wp_load_classic_theme_block_styles_on_demand(),
     2268 * this function is replaced by a closure in wp_hoist_late_printed_styles() which will capture the printing of
     2269 * two sets of "late" styles to be hoisted to the HEAD by means of the template enhancement output buffer:
     2270 *
     2271 * 1. Styles related to blocks are inserted right after the wp-block-library stylesheet.
     2272 * 2. All other styles are appended to the end of the HEAD.
     2273 *
     2274 * The closure calls print_footer_scripts() to print scripts in the footer as usual.
    22712275 *
    22722276 * @since 3.3.0
     
    36023606
    36033607    /*
    3604      * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally,
    3605      * and so that block-specific styles will only be enqueued when they are used on the page.
    3606      * A priority of zero allows for this to be easily overridden by themes which wish to opt out.
     3608     * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, and so
     3609     * that block-specific styles will only be enqueued when they are used on the page. A priority of zero allows for
     3610     * this to be easily overridden by themes which wish to opt out. If a site has explicitly opted out of loading
     3611     * separate block styles, then abort.
    36073612     */
    36083613    add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
     3614    if ( ! wp_should_load_separate_core_block_assets() ) {
     3615        return;
     3616    }
    36093617
    36103618    /*
    36113619     * Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
    3612      * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out.
     3620     * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. If a site
     3621     * has explicitly opted out of loading block styles on demand, then abort.
    36133622     */
    36143623    add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );
    3615 
    3616     // If a site has explicitly opted out of loading block styles on demand via filters with priorities higher than above, then abort.
    3617     if ( ! wp_should_load_separate_core_block_assets() || ! wp_should_load_block_assets_on_demand() ) {
     3624    if ( ! wp_should_load_block_assets_on_demand() ) {
    36183625        return;
    36193626    }
     
    36383645
    36393646    /*
    3640      * While normally late styles are printed, there is a filter to disable prevent this, so this makes sure they are
    3641      * printed. Note that this filter was intended to control whether to print the styles queued too late for the HTML
    3642      * head. This filter was introduced in <https://core.trac.wordpress.org/ticket/9346>. However, with the template
    3643      * enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can
    3644      * always hoist it to the HEAD.
    3645      */
    3646     add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX );
     3647     * Add a placeholder comment into the inline styles for wp-block-library, after which where the late block styles
     3648     * can be hoisted from the footer to be printed in the header by means of a filter below on the template enhancement
     3649     * output buffer. The `wp_print_styles` action is used to ensure that if the inline style gets replaced at
     3650     * `enqueue_block_assets` or `wp_enqueue_scripts` that the placeholder will be sure to be present.
     3651     */
     3652    $placeholder = sprintf( '/*%s*/', uniqid( 'wp_block_styles_on_demand_placeholder:' ) );
     3653    add_action(
     3654        'wp_print_styles',
     3655        static function () use ( $placeholder ) {
     3656            wp_add_inline_style( 'wp-block-library', $placeholder );
     3657        }
     3658    );
    36473659
    36483660    /*
    3649      * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header
    3650      * by means of a filter below on the template enhancement output buffer.
    3651      */
    3652     $placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) );
    3653 
    3654     wp_add_inline_style( 'wp-block-library', $placeholder );
    3655 
    3656     // Wrap print_late_styles() with a closure that captures the late-printed styles.
    3657     $printed_late_styles = '';
    3658     $capture_late_styles = static function () use ( &$printed_late_styles ) {
     3661     * Create a substitute for `print_late_styles()` which is aware of block styles. This substitute does not print
     3662     * the styles, but it captures what would be printed for block styles and non-block styles so that they can be
     3663     * later hoisted to the HEAD in the template enhancement output buffer. This will run at `wp_print_footer_scripts`
     3664     * before `print_footer_scripts()` is called.
     3665     */
     3666    $printed_block_styles = '';
     3667    $printed_late_styles  = '';
     3668    $capture_late_styles  = static function () use ( &$printed_block_styles, &$printed_late_styles ) {
     3669        // Gather the styles related to on-demand block enqueues.
     3670        $all_block_style_handles = array();
     3671        foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) {
     3672            foreach ( $block_type->style_handles as $style_handle ) {
     3673                $all_block_style_handles[] = $style_handle;
     3674            }
     3675        }
     3676        $all_block_style_handles = array_merge(
     3677            $all_block_style_handles,
     3678            array(
     3679                'global-styles',
     3680                'block-style-variation-styles',
     3681                'core-block-supports',
     3682                'core-block-supports-duotone',
     3683            )
     3684        );
     3685
     3686        /*
     3687         * First print all styles related to blocks which should inserted right after the wp-block-library stylesheet
     3688         * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`.
     3689         */
     3690        $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) );
     3691        if ( count( $enqueued_block_styles ) > 0 ) {
     3692            ob_start();
     3693            wp_styles()->do_items( $enqueued_block_styles );
     3694            $printed_block_styles = ob_get_clean();
     3695        }
     3696
     3697        /*
     3698         * Print all remaining styles not related to blocks. This contains a subset of the logic from
     3699         * `print_late_styles()`, without admin-specific logic and the `print_late_styles` filter to control whether
     3700         * late styles are printed (since they are being hoisted anyway).
     3701         */
    36593702        ob_start();
    3660         print_late_styles();
     3703        wp_styles()->do_footer_items();
    36613704        $printed_late_styles = ob_get_clean();
    36623705    };
    36633706
    36643707    /*
    3665      * If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts()
    3666      * was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the
     3708     * If `_wp_footer_scripts()` was unhooked from the `wp_print_footer_scripts` action, or if `wp_print_footer_scripts()`
     3709     * was unhooked from running at the `wp_footer` action, then only add a callback to `wp_footer` which will capture the
    36673710     * late-printed styles.
    36683711     *
    3669      * Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then
    3670      * swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before
     3712     * Otherwise, in the normal case where `_wp_footer_scripts()` will run at the `wp_print_footer_scripts` action, then
     3713     * swap out `_wp_footer_scripts()` with an alternative which captures the printed styles (for hoisting to HEAD) before
    36713714     * proceeding with printing the footer scripts.
    36723715     */
     
    36903733    add_filter(
    36913734        'wp_template_enhancement_output_buffer',
    3692         function ( $buffer ) use ( $placeholder, &$printed_late_styles ) {
     3735        static function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) {
    36933736
    36943737            // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
    36953738            $processor = new class( $buffer ) extends WP_HTML_Tag_Processor {
    3696                 public function get_span(): WP_HTML_Span {
    3697                     $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class.
    3698                     $instance->set_bookmark( 'here' );
    3699                     return $instance->bookmarks['here'];
     3739                /**
     3740                 * Gets the span for the current token.
     3741                 *
     3742                 * @return WP_HTML_Span Current token span.
     3743                 */
     3744                private function get_span(): WP_HTML_Span {
     3745                    // Note: This call will never fail according to the usage of this class, given it is always called after ::next_tag() is true.
     3746                    $this->set_bookmark( 'here' );
     3747                    return $this->bookmarks['here'];
     3748                }
     3749
     3750                /**
     3751                 * Inserts text before the current token.
     3752                 *
     3753                 * @param string $text Text to insert.
     3754                 */
     3755                public function insert_before( string $text ) {
     3756                    $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text );
     3757                }
     3758
     3759                /**
     3760                 * Inserts text after the current token.
     3761                 *
     3762                 * @param string $text Text to insert.
     3763                 */
     3764                public function insert_after( string $text ) {
     3765                    $span = $this->get_span();
     3766
     3767                    $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text );
     3768                }
     3769
     3770                /**
     3771                 * Removes the current token.
     3772                 */
     3773                public function remove() {
     3774                    $span = $this->get_span();
     3775
     3776                    $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' );
    37003777                }
    37013778            };
    37023779
    3703             // Loop over STYLE tags.
     3780            /*
     3781             * Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles
     3782             * at </head> (or else print everything there). The placeholder CSS comment will always be added to the
     3783             * wp-block-library inline style since it gets printed at `wp_head` before the blocks are rendered.
     3784             * This means that there may not actually be any block styles to hoist from the footer to insert after this
     3785             * inline style. The placeholder CSS comment needs to be added so that the inline style gets printed, but
     3786             * if the resulting inline style is empty after the placeholder is removed, then the inline style is
     3787             * removed.
     3788             */
    37043789            while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
    3705 
    3706                 // We've encountered the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment.
    37073790                if (
    3708                     ! $processor->is_tag_closer() &&
    37093791                    'STYLE' === $processor->get_tag() &&
    37103792                    'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
    37113793                ) {
    3712                     // If the inline style lacks the placeholder comment, then we have to continue until we get to </HEAD> to append the styles there.
    37133794                    $css_text = $processor->get_modifiable_text();
    3714                     if ( ! str_contains( $css_text, $placeholder ) ) {
    3715                         continue;
     3795
     3796                    /*
     3797                     * A placeholder CSS comment is added to the inline style in order to force an inline STYLE tag to
     3798                     * be printed. Now that we've located the inline style, the placeholder comment can be removed. If
     3799                     * there is no CSS left in the STYLE tag after removing the placeholder (aside from the sourceURL
     3800                     * comment, then remove the STYLE entirely.)
     3801                     */
     3802                    $css_text = str_replace( $placeholder, '', $css_text );
     3803                    if ( preg_match( ':^/\*# sourceURL=\S+? \*/$:', trim( $css_text ) ) ) {
     3804                        $processor->remove();
     3805                    } else {
     3806                        $processor->set_modifiable_text( $css_text );
    37163807                    }
    37173808
    3718                     // Remove the placeholder now that we've located the inline style.
    3719                     $processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) );
    3720                     $buffer = $processor->get_updated_html();
    3721 
    37223809                    // Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
    3723                     $span   = $processor->get_span();
    3724                     $buffer = implode(
    3725                         '',
    3726                         array(
    3727                             substr( $buffer, 0, $span->start + $span->length ),
    3728                             $printed_late_styles,
    3729                             substr( $buffer, $span->start + $span->length ),
    3730                         )
    3731                     );
    3732                     break;
    3733                 }
    3734 
    3735                 // As a fallback, append the hoisted late styles to the end of the HEAD.
    3736                 if ( $processor->is_tag_closer() && 'HEAD' === $processor->get_tag() ) {
    3737                     $span   = $processor->get_span();
    3738                     $buffer = implode(
    3739                         '',
    3740                         array(
    3741                             substr( $buffer, 0, $span->start ),
    3742                             $printed_late_styles,
    3743                             substr( $buffer, $span->start ),
    3744                         )
    3745                     );
     3810                    if ( '' !== $printed_block_styles ) {
     3811                        $processor->insert_after( $printed_block_styles );
     3812
     3813                        // Prevent printing them again at </head>.
     3814                        $printed_block_styles = '';
     3815                    }
     3816
     3817                    // If there aren't any late styles, there's no need to continue to finding </head>.
     3818                    if ( '' === $printed_late_styles ) {
     3819                        break;
     3820                    }
     3821                } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) {
     3822                    $processor->insert_before( $printed_block_styles . $printed_late_styles );
    37463823                    break;
    37473824                }
    37483825            }
    37493826
    3750             return $buffer;
     3827            return $processor->get_updated_html();
    37513828        }
    37523829    );
Note: See TracChangeset for help on using the changeset viewer.