Skip to content

Commit dd9ad0c

Browse files
authored
Merge branch 'WordPress:trunk' into trunk
2 parents a426964 + 608993d commit dd9ad0c

2 files changed

Lines changed: 330 additions & 2 deletions

File tree

src/wp-includes/class-wp-theme-json.php

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,8 +1296,32 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
12961296
*/
12971297
protected static function append_to_selector( $selector, $to_append ) {
12981298
if ( ! str_contains( $selector, ',' ) ) {
1299-
return $selector . $to_append;
1299+
return trim( $selector, " \t\n" ) . $to_append;
13001300
}
1301+
1302+
/**
1303+
* Check for an opportunity to skip the more-costly selector splitting.
1304+
* This should be possible if there are no comments, strings, functions,
1305+
* URLs, escapes, or comment declaration openers (CDOs).
1306+
*
1307+
* Note that this means the fast-path will not apply for selectors like
1308+
* the following incomplete list:
1309+
*
1310+
* - `[class ~= "wide"]`
1311+
* - `.wp-block:is(.is-style-a, .is-style-b)`
1312+
* - `:nth-child(1)`
1313+
*
1314+
* These syntax forms all present opportunities where a comma may not
1315+
* separate selectors. If none of the start characters are present,
1316+
* there should be no way for a comma to mean anything other than a
1317+
* comma token. The exception are syntax errors, which are not handled here.
1318+
*
1319+
* @see https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1320+
*/
1321+
if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1322+
return preg_replace( '~[ \t\n]*,[ \t\n]*~', "{$to_append}, ", trim( $selector, " \t\n" ) ) . $to_append;
1323+
}
1324+
13011325
$new_selectors = array();
13021326
$selectors = static::split_selector_list( $selector );
13031327
foreach ( $selectors as $sel ) {
@@ -1321,8 +1345,32 @@ protected static function append_to_selector( $selector, $to_append ) {
13211345
*/
13221346
protected static function prepend_to_selector( $selector, $to_prepend ) {
13231347
if ( ! str_contains( $selector, ',' ) ) {
1324-
return $to_prepend . $selector;
1348+
return $to_prepend . trim( $selector, " \t\n" );
13251349
}
1350+
1351+
/**
1352+
* Check for an opportunity to skip the more-costly selector splitting.
1353+
* This should be possible if there are no comments, strings, functions,
1354+
* URLs, escapes, or comment declaration openers (CDOs).
1355+
*
1356+
* Note that this means the fast-path will not apply for selectors like
1357+
* the following incomplete list:
1358+
*
1359+
* - `[class ~= "wide"]`
1360+
* - `.wp-block:is(.is-style-a, .is-style-b)`
1361+
* - `:nth-child(1)`
1362+
*
1363+
* These syntax forms all present opportunities where a comma may not
1364+
* separate selectors. If none of the start characters are present,
1365+
* there should be no way for a comma to mean anything other than a
1366+
* comma token. The exception are syntax errors, which are not handled here.
1367+
*
1368+
* @see https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1369+
*/
1370+
if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1371+
return $to_prepend . preg_replace( '~[ \t\n]*,[ \t\n]*~', ", {$to_prepend}", trim( $selector, " \t\n" ) );
1372+
}
1373+
13261374
$new_selectors = array();
13271375
$selectors = static::split_selector_list( $selector );
13281376
foreach ( $selectors as $sel ) {

tests/phpunit/tests/theme/wpThemeJson.php

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7731,4 +7731,284 @@ public function test_block_custom_states_ignores_non_whitelisted() {
77317731
$this->assertStringNotContainsString( '-current', $stylesheet_unsupported );
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
}

0 commit comments

Comments
 (0)