Make WordPress Core


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

Script Loader: Improve hoisted stylesheet ordering (in classic themes) to preserve CSS cascade.

This ensures that on-demand block styles are inserted right after the wp-block-library inline style whereas other stylesheets not related to blocks are appended to the end of the HEAD. This helps ensure the expected cascade is preserved. If no wp-block-library inline style is present, then all styles get appended to the HEAD regardless.

The handling of the CSS placeholder comment added to the wp-block-library inline style is also improved. It is now inserted later to ensure the inline style is printed. Additionally, when the CSS placeholder comment is removed from the wp-block-library inline style, the entire STYLE tag is now removed if there are no styles left (aside from the sourceURL comment).

Lastly, the use of the HTML Tag Processor is significantly improved to leverage WP_HTML_Text_Replacement.

Developed in https://github.com/WordPress/wordpress-develop/pull/10436

Follow-up to [61008].

Props westonruter, peterwilsoncc, dmsnell.
Fixes #64099.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/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.