Make WordPress Core


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.

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.