Make WordPress Core

Changeset 61174


Ignore:
Timestamp:
11/07/2025 04:27:45 AM (5 weeks 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.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/phpcompat.xml.dist

    r60800 r61174  
    115115    </rule>
    116116
     117    <!--
     118        Excluded while waiting for PHPCompatibility v10.
     119        See <https://github.com/PHPCompatibility/PHPCompatibility/issues/1481>.
     120    -->
     121    <rule ref="PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic">
     122        <exclude-pattern>/src/wp-includes/script-loader\.php$</exclude-pattern>
     123    </rule>
     124
    117125</ruleset>
  • 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    );
  • trunk/tests/phpunit/tests/template.php

    r61140 r61174  
    128128        $wp_scripts                = null;
    129129        $wp_styles                 = null;
    130         wp_scripts();
    131         wp_styles();
    132 
    133         $this->original_theme_features = $GLOBALS['_wp_theme_features'];
     130
    134131        foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) {
    135132            $this->original_ini_config[ $option ] = ini_get( $option );
     
    142139        $wp_styles  = $this->original_wp_styles;
    143140
    144         $GLOBALS['_wp_theme_features'] = $this->original_theme_features;
    145141        foreach ( $this->original_ini_config as $option => $value ) {
    146142            ini_set( $option, $value );
     
    150146        unregister_taxonomy( 'taxo' );
    151147        $this->set_permalink_structure( '' );
     148
    152149        parent::tear_down();
    153150    }
     
    631628            'wp_template_enhancement_output_buffer',
    632629            static function () {
    633                 return '<html>Hey!</html>';
     630                return '<html lang="en"><head><meta charset="utf-8"></head><body>Hey!</body></html>';
    634631            }
    635632        );
     
    14231420                },
    14241421                'expected_load_separate'  => false,
    1425                 'expected_on_demand'      => true,
     1422                'expected_on_demand'      => false,
    14261423                'expected_buffer_started' => false,
    14271424            ),
     
    14761473     * Data provider.
    14771474     *
    1478      * @return array<string, array{set_up: Closure|null}>
     1475     * @return array<string, array{set_up: Closure|null, inline_size_limit: int,  expected_styles: array{ HEAD: string[], BODY: string[] }}>
    14791476     */
    14801477    public function data_wp_hoist_late_printed_styles(): array {
     1478        $common_expected_head_styles = array(
     1479            'wp-img-auto-sizes-contain-inline-css',
     1480            'early-css',
     1481            'early-inline-css',
     1482            'wp-emoji-styles-inline-css',
     1483            'wp-block-library-css',
     1484            'wp-block-separator-css',
     1485            'global-styles-inline-css',
     1486            'core-block-supports-inline-css',
     1487            'classic-theme-styles-css',
     1488            'normal-css',
     1489            'normal-inline-css',
     1490            'wp-custom-css',
     1491            'late-css',
     1492            'late-inline-css',
     1493        );
     1494
    14811495        return array(
    1482             'no_actions_removed'              => array(
    1483                 'set_up' => null,
    1484             ),
    1485             '_wp_footer_scripts_removed'      => array(
    1486                 'set_up' => static function () {
     1496            'standard_classic_theme_config_with_min_styles_inlined' => array(
     1497                'set_up'            => null,
     1498                'inline_size_limit' => 0,
     1499                'expected_styles'   => array(
     1500                    'HEAD' => $common_expected_head_styles,
     1501                    'BODY' => array(),
     1502                ),
     1503            ),
     1504            'standard_classic_theme_config_with_max_styles_inlined' => array(
     1505                'set_up'            => null,
     1506                'inline_size_limit' => PHP_INT_MAX,
     1507                'expected_styles'   => array(
     1508                    'HEAD' => array(
     1509                        'wp-img-auto-sizes-contain-inline-css',
     1510                        'early-css',
     1511                        'early-inline-css',
     1512                        'wp-emoji-styles-inline-css',
     1513                        'wp-block-library-inline-css',
     1514                        'wp-block-separator-inline-css',
     1515                        'global-styles-inline-css',
     1516                        'core-block-supports-inline-css',
     1517                        'classic-theme-styles-inline-css',
     1518                        'normal-css',
     1519                        'normal-inline-css',
     1520                        'wp-custom-css',
     1521                        'late-css',
     1522                        'late-inline-css',
     1523                    ),
     1524                    'BODY' => array(),
     1525                ),
     1526            ),
     1527            'standard_classic_theme_config_extra_block_library_inline_style' => array(
     1528                'set_up'            => static function () {
     1529                    add_action(
     1530                        'enqueue_block_assets',
     1531                        static function () {
     1532                            wp_add_inline_style( 'wp-block-library', '/* Extra CSS which prevents empty inline style containing placeholder from being removed. */' );
     1533                        }
     1534                    );
     1535                },
     1536                'inline_size_limit' => 0,
     1537                'expected_styles'   => array(
     1538                    'HEAD' => ( function ( $expected_styles ) {
     1539                        // Insert 'wp-block-library-inline-css' right after 'wp-block-library-css'.
     1540                        $i = array_search( 'wp-block-library-css', $expected_styles, true );
     1541                        $this->assertIsInt( $i, 'Expected wp-block-library-css to be among the styles.' );
     1542                        array_splice( $expected_styles, $i + 1, 0, 'wp-block-library-inline-css' );
     1543                        return $expected_styles;
     1544                    } )( $common_expected_head_styles ),
     1545                    'BODY' => array(),
     1546                ),
     1547            ),
     1548            'classic_theme_opt_out_separate_block_styles' => array(
     1549                'set_up'            => static function () {
     1550                    add_filter( 'should_load_separate_core_block_assets', '__return_false' );
     1551                },
     1552                'inline_size_limit' => 0,
     1553                'expected_styles'   => array(
     1554                    'HEAD' => array(
     1555                        'wp-img-auto-sizes-contain-inline-css',
     1556                        'early-css',
     1557                        'early-inline-css',
     1558                        'wp-emoji-styles-inline-css',
     1559                        'wp-block-library-css',
     1560                        'classic-theme-styles-css',
     1561                        'global-styles-inline-css',
     1562                        'normal-css',
     1563                        'normal-inline-css',
     1564                        'wp-custom-css',
     1565                    ),
     1566                    'BODY' => array(
     1567                        'late-css',
     1568                        'late-inline-css',
     1569                        'core-block-supports-inline-css',
     1570                    ),
     1571                ),
     1572            ),
     1573            '_wp_footer_scripts_removed'                  => array(
     1574                'set_up'            => static function () {
    14871575                    remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
    14881576                },
    1489             ),
    1490             'wp_print_footer_scripts_removed' => array(
    1491                 'set_up' => static function () {
     1577                'inline_size_limit' => 0,
     1578                'expected_styles'   => array(
     1579                    'HEAD' => $common_expected_head_styles,
     1580                    'BODY' => array(),
     1581                ),
     1582            ),
     1583            'wp_print_footer_scripts_removed'             => array(
     1584                'set_up'            => static function () {
    14921585                    remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
    14931586                },
    1494             ),
    1495             'both_actions_removed'            => array(
    1496                 'set_up' => static function () {
     1587                'inline_size_limit' => 0,
     1588                'expected_styles'   => array(
     1589                    'HEAD' => $common_expected_head_styles,
     1590                    'BODY' => array(),
     1591                ),
     1592            ),
     1593            'both_actions_removed'                        => array(
     1594                'set_up'            => static function () {
    14971595                    remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
    14981596                    remove_action( 'wp_footer', 'wp_print_footer_scripts' );
    14991597                },
    1500             ),
    1501             'block_library_removed'           => array(
    1502                 'set_up' => static function () {
    1503                     wp_deregister_style( 'wp-block-library' );
    1504                 },
     1598                'inline_size_limit' => 0,
     1599                'expected_styles'   => array(
     1600                    'HEAD' => $common_expected_head_styles,
     1601                    'BODY' => array(),
     1602                ),
     1603            ),
     1604            'disable_block_library'                       => array(
     1605                'set_up'            => static function () {
     1606                    add_action(
     1607                        'enqueue_block_assets',
     1608                        function (): void {
     1609                            wp_deregister_style( 'wp-block-library' );
     1610                            wp_register_style( 'wp-block-library', '' );
     1611                        }
     1612                    );
     1613                    add_filter( 'should_load_separate_core_block_assets', '__return_false' );
     1614                },
     1615                'inline_size_limit' => 0,
     1616                'expected_styles'   => array(
     1617                    'HEAD' => array(
     1618                        'wp-img-auto-sizes-contain-inline-css',
     1619                        'early-css',
     1620                        'early-inline-css',
     1621                        'wp-emoji-styles-inline-css',
     1622                        'classic-theme-styles-css',
     1623                        'global-styles-inline-css',
     1624                        'normal-css',
     1625                        'normal-inline-css',
     1626                        'wp-custom-css',
     1627                    ),
     1628                    'BODY' => array(
     1629                        'late-css',
     1630                        'late-inline-css',
     1631                        'core-block-supports-inline-css',
     1632                    ),
     1633                ),
     1634            ),
     1635            'override_block_library_inline_style_late'    => array(
     1636                'set_up'            => static function () {
     1637                    add_action(
     1638                        'enqueue_block_assets',
     1639                        function (): void {
     1640                            // This tests what happens when the placeholder comment gets replaced unexpectedly.
     1641                            wp_styles()->registered['wp-block-library']->extra['after'] = array( '/* OVERRIDDEN! */' );
     1642                        }
     1643                    );
     1644                },
     1645                'inline_size_limit' => 0,
     1646                'expected_styles'   => array(
     1647                    'HEAD' => array(
     1648                        'wp-img-auto-sizes-contain-inline-css',
     1649                        'early-css',
     1650                        'early-inline-css',
     1651                        'wp-emoji-styles-inline-css',
     1652                        'wp-block-library-css',
     1653                        'wp-block-library-inline-css', // This contains the "OVERRIDDEN" text.
     1654                        'wp-block-separator-css',
     1655                        'global-styles-inline-css',
     1656                        'core-block-supports-inline-css',
     1657                        'classic-theme-styles-css',
     1658                        'normal-css',
     1659                        'normal-inline-css',
     1660                        'wp-custom-css',
     1661                        'late-css',
     1662                        'late-inline-css',
     1663                    ),
     1664                    'BODY' => array(),
     1665                ),
    15051666            ),
    15061667        );
     
    15111672     *
    15121673     * @ticket 64099
     1674     * @covers ::wp_load_classic_theme_block_styles_on_demand
    15131675     * @covers ::wp_hoist_late_printed_styles
    15141676     *
    15151677     * @dataProvider data_wp_hoist_late_printed_styles
    15161678     */
    1517     public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
     1679    public function test_wp_hoist_late_printed_styles( ?Closure $set_up, int $inline_size_limit, array $expected_styles ): void {
     1680        switch_theme( 'default' );
     1681        global $wp_styles;
     1682        $wp_styles = null;
     1683
     1684        // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots.
     1685        add_filter(
     1686            'styles_inline_size_limit',
     1687            static function () use ( $inline_size_limit ): int {
     1688                return $inline_size_limit;
     1689            }
     1690        );
     1691
     1692        add_filter(
     1693            'wp_get_custom_css',
     1694            static function () {
     1695                return '/* CUSTOM CSS from Customizer */';
     1696            }
     1697        );
     1698
    15181699        if ( $set_up ) {
    15191700            $set_up();
    15201701        }
    15211702
    1522         switch_theme( 'default' );
    1523 
    1524         // Enqueue a style
    1525         wp_enqueue_style( 'early', 'http://example.com/style.css' );
     1703        wp_load_classic_theme_block_styles_on_demand();
     1704
     1705        // Ensure that separate core block assets get registered.
     1706        register_core_block_style_handles();
     1707        $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' );
     1708
     1709        // Ensure stylesheet files exist on the filesystem since a build may not have been done.
     1710        $this->ensure_style_asset_file_created(
     1711            'wp-block-library',
     1712            wp_should_load_separate_core_block_assets() ? 'css/dist/block-library/common.css' : 'css/dist/block-library/style.css'
     1713        );
     1714        if ( wp_should_load_separate_core_block_assets() ) {
     1715            $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' );
     1716        }
     1717        $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' );
     1718
     1719        // Enqueue a style early, before wp_enqueue_scripts.
     1720        wp_enqueue_style( 'early', 'https://example.com/style.css' );
    15261721        wp_add_inline_style( 'early', '/* EARLY */' );
    15271722
    1528         wp_hoist_late_printed_styles();
    1529 
    1530         // Ensure late styles are printed.
    1531         add_filter( 'print_late_styles', '__return_false', 1000 );
    1532         $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' );
     1723        // Enqueue a style at the normal spot.
     1724        add_action(
     1725            'wp_enqueue_scripts',
     1726            static function () {
     1727                wp_enqueue_style( 'normal', 'https://example.com/normal.css' );
     1728                wp_add_inline_style( 'normal', '/* NORMAL */' );
     1729            }
     1730        );
     1731
     1732        // Call wp_hoist_late_printed_styles() if wp_load_classic_theme_block_styles_on_demand() queued it up.
     1733        if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) {
     1734            wp_hoist_late_printed_styles();
     1735        }
    15331736
    15341737        // Simulate wp_head.
     
    15381741
    15391742        // Enqueue a late style (after wp_head).
    1540         wp_enqueue_style( 'late', 'http://example.com/late-style.css', array(), null );
    1541         wp_add_inline_style( 'late', '/* EARLY */' );
     1743        wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null );
     1744        wp_add_inline_style( 'late', '/* LATE */' );
     1745
     1746        // Simulate the_content().
     1747        $content = apply_filters(
     1748            'the_content',
     1749            '<!-- wp:separator --><hr class="wp-block-separator has-alpha-channel-opacity"/><!-- /wp:separator -->'
     1750        );
    15421751
    15431752        // Simulate footer scripts.
     
    15451754
    15461755        // Create a simulated output buffer.
    1547         $buffer = '<html><head>' . $head_output . '</head><body><main>Content</main>' . $footer_output . '</body></html>';
     1756        $buffer = '<html lang="en"><head><meta charset="utf-8">' . $head_output . '</head><body><main>' . $content . '</main>' . $footer_output . '</body></html>';
     1757
     1758        $placeholder_regexp = '#/\*wp_block_styles_on_demand_placeholder:[a-f0-9]+\*/#';
     1759        if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) {
     1760            $this->assertMatchesRegularExpression( $placeholder_regexp, $buffer, 'Expected the placeholder to be present in the buffer.' );
     1761        }
    15481762
    15491763        // Apply the output buffer filter.
    15501764        $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer );
    15511765
    1552         $this->assertStringContainsString( '</head>', $buffer, 'Expected the closing HEAD tag to be in the response.' );
    1553 
    1554         $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9-]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' );
     1766        $this->assertStringContainsString( '</head>', $filtered_buffer, 'Expected the closing HEAD tag to be in the response.' );
     1767
     1768        $this->assertDoesNotMatchRegularExpression( $placeholder_regexp, $filtered_buffer, 'Expected the placeholder to be removed.' );
    15551769        $found_styles = array(
    15561770            'HEAD' => array(),
     
    15701784        }
    15711785
    1572         $expected = array(
    1573             'early-css',
    1574             'early-inline-css',
    1575             'late-css',
    1576             'late-inline-css',
    1577         );
    1578         foreach ( $expected as $style_id ) {
    1579             $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD.' );
    1580         }
     1786        /*
     1787         * Since new styles could appear at any time and since certain styles leak in from the global scope not being
     1788         * properly reset somewhere else in the test suite, we only check that the expected styles are at least present
     1789         * and in the same order. When new styles are introduced in core, they may be added to this array as opposed to
     1790         * updating the arrays in the data provider, if appropriate.
     1791         */
     1792        $ignored_styles = array(
     1793            'core-block-supports-duotone-inline-css',
     1794            'wp-block-library-theme-css',
     1795            'wp-block-template-skip-link-inline-css',
     1796        );
     1797
     1798        $found_subset_styles = array();
     1799        foreach ( array( 'HEAD', 'BODY' ) as $group ) {
     1800            $found_subset_styles[ $group ] = array_values( array_diff( $found_styles[ $group ], $ignored_styles ) );
     1801        }
     1802
    15811803        $this->assertSame(
    1582             $expected,
    1583             array_values( array_intersect( $found_styles['HEAD'], $expected ) ),
    1584             'Expected styles to be printed in the same order.'
    1585         );
    1586         $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' );
     1804            $expected_styles,
     1805            $found_subset_styles,
     1806            'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_subset_styles )
     1807        );
     1808    }
     1809
     1810    /**
     1811     * Ensures a CSS file is on the filesystem.
     1812     *
     1813     * This is needed because unit tests may be run without a build step having been done. Something similar can be seen
     1814     * elsewhere in tests for the `wp-emoji-loader.js` script:
     1815     *
     1816     *     self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' );
     1817     *
     1818     * @param string $handle        Style handle.
     1819     * @param string $relative_path Relative path to the CSS file in wp-includes.
     1820     *
     1821     * @throws Exception If the supplied style handle is not registered as expected.
     1822     */
     1823    private function ensure_style_asset_file_created( string $handle, string $relative_path ) {
     1824        $dependency = wp_styles()->query( $handle );
     1825        if ( ! $dependency ) {
     1826            throw new Exception( "The stylesheet for $handle is not registered." );
     1827        }
     1828        $dependency->src = includes_url( $relative_path );
     1829        $path            = ABSPATH . WPINC . '/' . $relative_path;
     1830        if ( ! file_exists( $path ) ) {
     1831            $dir = dirname( $path );
     1832            if ( ! file_exists( $dir ) ) {
     1833                mkdir( $dir, 0777, true );
     1834            }
     1835            file_put_contents( $path, "/* CSS for $handle */" );
     1836        }
     1837        wp_style_add_data( $handle, 'path', $path );
    15871838    }
    15881839
     
    15921843
    15931844        $this->assertSame( $expected, $hierarchy, $message );
     1845    }
     1846
     1847    /**
     1848     * Exports PHP array as string formatted as a snapshot for pasting into a data provider.
     1849     *
     1850     * Unfortunately, `var_export()` always includes array indices even for lists. For example:
     1851     *
     1852     *     var_export( array( 'a', 'b', 'c' ) );
     1853     *
     1854     * Results in:
     1855     *
     1856     *     array (
     1857     *       0 => 'a',
     1858     *       1 => 'b',
     1859     *       2 => 'c',
     1860     *     )
     1861     *
     1862     * This makes it unhelpful when outputting a snapshot to update a unit test. So this function strips out the indices
     1863     * to facilitate copy/pasting the snapshot from an assertion error message into the data provider. For example:
     1864     *
     1865     *      array(
     1866     *          'a',
     1867     *          'b',
     1868     *          'c',
     1869     *      )
     1870     *
     1871     *
     1872     * @param array $snapshot Snapshot.
     1873     * @return string Snapshot export.
     1874     */
     1875    private static function get_array_snapshot_export( array $snapshot ): string {
     1876        $export = var_export( $snapshot, true );
     1877        $export = preg_replace( '/\barray \($/m', 'array(', $export );
     1878        $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export );
     1879        $export = preg_replace( '/=> *\n +/', '=> ', $export );
     1880        $export = preg_replace( '/array\(\n\s+\)/', 'array()', $export );
     1881        return preg_replace_callback(
     1882            '/(^ +)/m',
     1883            static function ( $matches ) {
     1884                return str_repeat( "\t", strlen( $matches[0] ) / 2 );
     1885            },
     1886            $export
     1887        );
    15941888    }
    15951889
Note: See TracChangeset for help on using the changeset viewer.