Make WordPress Core

Changeset 61174


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

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.