Make WordPress Core


Ignore:
Timestamp:
07/07/2026 12:20:57 AM (3 months ago)
Author:
dmsnell
Message:

Editor: Skip parsing in prepend_to_selector() for simple selectors.

Splitting a CSS selector involves unnecessary computation and allocation when the selector could not possibly involve more than one selector. It was discovered in profiling that this function was being called 4000+ times in a single request, and adding a pre-condition before splitting saved meaningful time on a page render.

This patch adds the pre-condition to verify that any commas present in a selector string can only be “comma tokens,” which split selectors at the top level, making it safe to fall back to simpler parsing that’s more efficient than general separator splitting.

This commit synchronizes work from the following Gutenberg PRs:

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/12306
Discussed in: https://core.trac.wordpress.org/ticket/65533

Follow-up to [62607].

Props dmsnell, josephscott.
See #65533.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r62641 r62650  
    77327732                $this->assertStringNotContainsString( 'current-menu-item', $stylesheet_unsupported );
    77337733        }
     7734
     7735        /**
     7736         * Tests that append_to_selector correctly appends to single and
     7737         * compound (comma-separated) selectors.
     7738         *
     7739         * @ticket 65533
     7740         *
     7741         * @dataProvider data_append_to_selector
     7742         *
     7743         * @param string $selector  Original CSS selector.
     7744         * @param string $to_append Selector to append.
     7745         * @param string $expected  Expected resulting selector.
     7746         */
     7747        public function test_append_to_selector( $selector, $to_append, $expected ) {
     7748                $theme_json = new ReflectionClass( 'WP_Theme_JSON' );
     7749
     7750                $func = $theme_json->getMethod( 'append_to_selector' );
     7751                if ( PHP_VERSION_ID < 80100 ) {
     7752                        $func->setAccessible( true );
     7753                }
     7754
     7755                $actual = $func->invoke( null, $selector, $to_append );
     7756
     7757                $this->assertSame( $expected, $actual );
     7758        }
     7759
     7760        /**
     7761         * Data provider.
     7762         *
     7763         * @return array<string, {selector: string, to_append: string, expeted: string}>
     7764         */
     7765        public function data_append_to_selector() {
     7766                return array(
     7767                        'single class selector'                        => array(
     7768                                'selector'  => '.inner',
     7769                                'to_append' => ':hover',
     7770                                'expected'  => '.inner:hover',
     7771                        ),
     7772                        'single element selector'                      => array(
     7773                                'selector'  => 'p',
     7774                                'to_append' => ':hover',
     7775                                'expected'  => 'p:hover',
     7776                        ),
     7777                        'single id selector'                           => array(
     7778                                'selector'  => '#main',
     7779                                'to_append' => ':focus',
     7780                                'expected'  => '#main:focus',
     7781                        ),
     7782                        'two comma-separated selectors without spaces' => array(
     7783                                'selector'  => 'h1,h2',
     7784                                'to_append' => ':hover',
     7785                                'expected'  => 'h1:hover, h2:hover',
     7786                        ),
     7787                        'three comma-separated selectors without spaces' => array(
     7788                                'selector'  => 'h1,h2,h3',
     7789                                'to_append' => ':focus',
     7790                                'expected'  => 'h1:focus, h2:focus, h3:focus',
     7791                        ),
     7792                        'comma-separated class selectors without spaces' => array(
     7793                                'selector'  => '.foo,.bar',
     7794                                'to_append' => '.is-active',
     7795                                'expected'  => '.foo.is-active, .bar.is-active',
     7796                        ),
     7797                        'append without leading pseudo'                => array(
     7798                                'selector'  => '.child',
     7799                                'to_append' => '.parent',
     7800                                'expected'  => '.child.parent',
     7801                        ),
     7802                        'compound selector with class append no comma spaces' => array(
     7803                                'selector'  => '.a,.b',
     7804                                'to_append' => '.parent',
     7805                                'expected'  => '.a.parent, .b.parent',
     7806                        ),
     7807                        'descendant selector appended'                 => array(
     7808                                'selector'  => '.block .inner',
     7809                                'to_append' => ':hover',
     7810                                'expected'  => '.block .inner:hover',
     7811                        ),
     7812                        'descendant selectors comma-separated without spaces' => array(
     7813                                'selector'  => '.block .inner,.block .alt',
     7814                                'to_append' => ':hover',
     7815                                'expected'  => '.block .inner:hover, .block .alt:hover',
     7816                        ),
     7817                        'empty selector'                               => array(
     7818                                'selector'  => '',
     7819                                'to_append' => ':hover',
     7820                                'expected'  => ':hover',
     7821                        ),
     7822                        'empty append'                                 => array(
     7823                                'selector'  => '.child',
     7824                                'to_append' => '',
     7825                                'expected'  => '.child',
     7826                        ),
     7827                        'both empty'                                   => array(
     7828                                'selector'  => '',
     7829                                'to_append' => '',
     7830                                'expected'  => '',
     7831                        ),
     7832                        'attribute selector'                           => array(
     7833                                'selector'  => '[data-type="example"]',
     7834                                'to_append' => ':hover',
     7835                                'expected'  => '[data-type="example"]:hover',
     7836                        ),
     7837                        'pseudo-class selector'                        => array(
     7838                                'selector'  => ':where(.is-layout-flex)',
     7839                                'to_append' => ':hover',
     7840                                'expected'  => ':where(.is-layout-flex):hover',
     7841                        ),
     7842                        'many comma-separated selectors without spaces' => array(
     7843                                'selector'  => 'h1,h2,h3,h4,h5,h6',
     7844                                'to_append' => ':hover',
     7845                                'expected'  => 'h1:hover, h2:hover, h3:hover, h4:hover, h5:hover, h6:hover',
     7846                        ),
     7847                        'real world block element selector'            => array(
     7848                                'selector'  => 'p',
     7849                                'to_append' => '.is-style-custom',
     7850                                'expected'  => 'p.is-style-custom',
     7851                        ),
     7852                        'real world compound block element selectors'  => array(
     7853                                'selector'  => 'a,.wp-element-button',
     7854                                'to_append' => '.is-style-custom',
     7855                                'expected'  => 'a.is-style-custom, .wp-element-button.is-style-custom',
     7856                        ),
     7857                        'spaces after commas are normalized'           => array(
     7858                                'selector'  => 'h1, h2, h3',
     7859                                'to_append' => ':hover',
     7860                                'expected'  => 'h1:hover, h2:hover, h3:hover',
     7861                        ),
     7862                        'spaces after commas normalized with class selectors' => array(
     7863                                'selector'  => '.foo, .bar',
     7864                                'to_append' => '.is-active',
     7865                                'expected'  => '.foo.is-active, .bar.is-active',
     7866                        ),
     7867                        'mixed whitespace around commas is normalized' => array(
     7868                                'selector'  => '.a ,  .b , .c',
     7869                                'to_append' => ':hover',
     7870                                'expected'  => '.a:hover, .b:hover, .c:hover',
     7871                        ),
     7872                );
     7873        }
     7874
     7875        /**
     7876         * Tests that prepend_to_selector correctly prepends to single and
     7877         * compound (comma-separated) selectors.
     7878         *
     7879         * @ticket 65533
     7880         *
     7881         * @dataProvider data_prepend_to_selector
     7882         *
     7883         * @param string $selector   Original CSS selector.
     7884         * @param string $to_prepend Selector to prepend.
     7885         * @param string $expected   Expected resulting selector.
     7886         */
     7887        public function test_prepend_to_selector( $selector, $to_prepend, $expected ) {
     7888                $theme_json = new ReflectionClass( 'WP_Theme_JSON' );
     7889
     7890                $func = $theme_json->getMethod( 'prepend_to_selector' );
     7891                if ( PHP_VERSION_ID < 80100 ) {
     7892                        $func->setAccessible( true );
     7893                }
     7894
     7895                $actual = $func->invoke( null, $selector, $to_prepend );
     7896
     7897                $this->assertSame( $expected, $actual );
     7898        }
     7899
     7900        /**
     7901         * Data provider.
     7902         *
     7903         * @return array<string, {selector: string, to_prepend: string, expeted: string}>
     7904         */
     7905        public function data_prepend_to_selector() {
     7906                return array(
     7907                        'single class selector'                        => array(
     7908                                'selector'   => '.inner',
     7909                                'to_prepend' => '.wrapper ',
     7910                                'expected'   => '.wrapper .inner',
     7911                        ),
     7912                        'single element selector'                      => array(
     7913                                'selector'   => 'p',
     7914                                'to_prepend' => '.wrapper ',
     7915                                'expected'   => '.wrapper p',
     7916                        ),
     7917                        'single id selector'                           => array(
     7918                                'selector'   => '#main',
     7919                                'to_prepend' => '.wrapper ',
     7920                                'expected'   => '.wrapper #main',
     7921                        ),
     7922                        'two comma-separated selectors without spaces' => array(
     7923                                'selector'   => 'h1,h2',
     7924                                'to_prepend' => '.wrapper ',
     7925                                'expected'   => '.wrapper h1, .wrapper h2',
     7926                        ),
     7927                        'three comma-separated selectors without spaces' => array(
     7928                                'selector'   => 'h1,h2,h3',
     7929                                'to_prepend' => '.some-class ',
     7930                                'expected'   => '.some-class h1, .some-class h2, .some-class h3',
     7931                        ),
     7932                        'comma-separated class selectors without spaces' => array(
     7933                                'selector'   => '.foo,.bar',
     7934                                'to_prepend' => '.prefix ',
     7935                                'expected'   => '.prefix .foo, .prefix .bar',
     7936                        ),
     7937                        'prepend without trailing space'               => array(
     7938                                'selector'   => '.child',
     7939                                'to_prepend' => '.parent',
     7940                                'expected'   => '.parent.child',
     7941                        ),
     7942                        'compound selector without trailing space no comma spaces' => array(
     7943                                'selector'   => '.a,.b',
     7944                                'to_prepend' => '.parent',
     7945                                'expected'   => '.parent.a, .parent.b',
     7946                        ),
     7947                        'descendant selector prepended'                => array(
     7948                                'selector'   => '.block .inner',
     7949                                'to_prepend' => '.scope ',
     7950                                'expected'   => '.scope .block .inner',
     7951                        ),
     7952                        'descendant selectors comma-separated without spaces' => array(
     7953                                'selector'   => '.block .inner,.block .alt',
     7954                                'to_prepend' => '.scope ',
     7955                                'expected'   => '.scope .block .inner, .scope .block .alt',
     7956                        ),
     7957                        'empty selector'                               => array(
     7958                                'selector'   => '',
     7959                                'to_prepend' => '.prefix ',
     7960                                'expected'   => '.prefix ',
     7961                        ),
     7962                        'empty prepend'                                => array(
     7963                                'selector'   => '.child',
     7964                                'to_prepend' => '',
     7965                                'expected'   => '.child',
     7966                        ),
     7967                        'both empty'                                   => array(
     7968                                'selector'   => '',
     7969                                'to_prepend' => '',
     7970                                'expected'   => '',
     7971                        ),
     7972                        'attribute selector'                           => array(
     7973                                'selector'   => '[data-type="example"]',
     7974                                'to_prepend' => '.scope ',
     7975                                'expected'   => '.scope [data-type="example"]',
     7976                        ),
     7977                        'pseudo-class selector'                        => array(
     7978                                'selector'   => ':where(.is-layout-flex)',
     7979                                'to_prepend' => '.editor ',
     7980                                'expected'   => '.editor :where(.is-layout-flex)',
     7981                        ),
     7982                        'many comma-separated selectors without spaces' => array(
     7983                                'selector'   => 'h1,h2,h3,h4,h5,h6',
     7984                                'to_prepend' => '.content ',
     7985                                'expected'   => '.content h1, .content h2, .content h3, .content h4, .content h5, .content h6',
     7986                        ),
     7987                        'real world block element selector'            => array(
     7988                                'selector'   => 'p',
     7989                                'to_prepend' => '.wp-block-group ',
     7990                                'expected'   => '.wp-block-group p',
     7991                        ),
     7992                        'real world compound block element selectors'  => array(
     7993                                'selector'   => 'a,.wp-element-button',
     7994                                'to_prepend' => '.wp-block-group ',
     7995                                'expected'   => '.wp-block-group a, .wp-block-group .wp-element-button',
     7996                        ),
     7997                        'spaces after commas are normalized'           => array(
     7998                                'selector'   => 'h1, h2, h3',
     7999                                'to_prepend' => '.some-class ',
     8000                                'expected'   => '.some-class h1, .some-class h2, .some-class h3',
     8001                        ),
     8002                        'spaces after commas normalized with class selectors' => array(
     8003                                'selector'   => '.foo, .bar',
     8004                                'to_prepend' => '.prefix ',
     8005                                'expected'   => '.prefix .foo, .prefix .bar',
     8006                        ),
     8007                        'mixed whitespace around commas are normalized' => array(
     8008                                'selector'   => '.a ,  .b , .c',
     8009                                'to_prepend' => '.pre ',
     8010                                'expected'   => '.pre .a, .pre .b, .pre .c',
     8011                        ),
     8012                );
     8013        }
    77348014}
Note: See TracChangeset for help on using the changeset viewer.