diff --git a/src/wp-includes/html-api/class-wp-css-selectors.php b/src/wp-includes/html-api/class-wp-css-selectors.php
index 8ccec5de029cc..734c3e38d094b 100644
--- a/src/wp-includes/html-api/class-wp-css-selectors.php
+++ b/src/wp-includes/html-api/class-wp-css-selectors.php
@@ -117,21 +117,31 @@ private static function parse( string $input ) {
$input = str_replace( array( "\r", "\f" ), "\n", $input );
$input = str_replace( "\0", "\u{FFFD}", $input );
- $length = strlen( $input );
- $selectors = array();
-
$offset = 0;
- while ( $offset < $length ) {
- $selector = WP_CSS_ID_Selector::parse( $input, $offset );
- if ( null !== $selector ) {
- $selectors[] = $selector;
- }
+ $selector = WP_CSS_Complex_Selector::parse( $input, $offset );
+ if ( null === $selector ) {
+ return null;
}
- if ( count( $selectors ) ) {
- return new WP_CSS_Selector_List( $selectors );
+ WP_CSS_Selector_Parser::parse_whitespace( $input, $offset );
+
+ $selectors = array( $selector );
+ while ( $offset < strlen( $input ) ) {
+ // Each loop should stop on a `,` selector list delimiter.
+ if ( ',' !== $input[ $offset ] ) {
+ return null;
+ }
+ ++$offset;
+ WP_CSS_Selector_Parser::parse_whitespace( $input, $offset );
+ $selector = WP_CSS_Complex_Selector::parse( $input, $offset );
+ if ( null === $selector ) {
+ return null;
+ }
+ $selectors[] = $selector;
+ WP_CSS_Selector_Parser::parse_whitespace( $input, $offset );
}
- return null;
+
+ return new WP_CSS_Selector_List( $selectors );
}
}
@@ -145,7 +155,7 @@ public static function parse( string $input, int &$offset );
abstract class WP_CSS_Selector_Parser implements IWP_CSS_Selector_Parser {
const UTF8_MAX_CODEPOINT_VALUE = 0x10FFFF;
- protected static function parse_whitespace( string $input, int &$offset ): bool {
+ public static function parse_whitespace( string $input, int &$offset ): bool {
$length = strspn( $input, " \t\r\n\f", $offset );
$advanced = $length > 0;
$offset += $length;
@@ -938,35 +948,38 @@ public static function parse( string $input, int &$offset ): ?self {
$found_whitespace = self::parse_whitespace( $input, $updated_offset );
while ( $updated_offset < strlen( $input ) ) {
- switch ( $input[ $updated_offset ] ) {
- case self::COMBINATOR_CHILD:
- case self::COMBINATOR_NEXT_SIBLING:
- case self::COMBINATOR_SUBSEQUENT_SIBLING:
+ if (
+ self::COMBINATOR_CHILD === $input[ $updated_offset ] ||
+ self::COMBINATOR_NEXT_SIBLING === $input[ $updated_offset ] ||
+ self::COMBINATOR_SUBSEQUENT_SIBLING === $input[ $updated_offset ]
+ ) {
$combinator = $input[ $updated_offset ];
++$updated_offset;
self::parse_whitespace( $input, $updated_offset );
- break;
- default:
- /*
- * Whitespace is a descendant combinator.
- * Either whitespace was found and we're on a selector,
- * or we've failed to find any combinator and parsing is complete.
- */
- if ( ! $found_whitespace ) {
- break 2;
- }
- $combinator = self::COMBINATOR_DESCENDANT;
+ // Failure to find a selector here is a parse error
+ $selector = WP_CSS_Selector::parse( $input, $updated_offset );
+ // Failure to find a selector is a parse error.
+ if ( null === $selector ) {
+ return null;
+ }
+ $selectors[] = $combinator;
+ $selectors[] = $selector;
+ } elseif ( ! $found_whitespace ) {
+ break;
+ } else {
+
+ /*
+ * Whitespace is ambiguous, it could be a descendant combinator or
+ * insignificant whitespace.
+ */
+ $selector = WP_CSS_Selector::parse( $input, $updated_offset );
+ if ( null === $selector ) {
break;
+ }
+ $selectors[] = self::COMBINATOR_DESCENDANT;
+ $selectors[] = $selector;
}
- // Here we've found a combinator and need another selector.
- $selector = WP_CSS_Selector::parse( $input, $updated_offset );
- // Failure to find a selector is a parse error.
- if ( null === $selector ) {
- return null;
- }
- $selectors[] = $combinator;
- $selectors[] = $selector;
$found_whitespace = self::parse_whitespace( $input, $updated_offset );
}
$offset = $updated_offset;
diff --git a/tests/phpunit/tests/html-api/wpCssSelectors.php b/tests/phpunit/tests/html-api/wpCssSelectors.php
index 4189ec586011a..33ada4ccbe3f9 100644
--- a/tests/phpunit/tests/html-api/wpCssSelectors.php
+++ b/tests/phpunit/tests/html-api/wpCssSelectors.php
@@ -357,15 +357,24 @@ public function test_parse_selector() {
$this->assertSame( ' > .child', substr( $input, $offset ) );
}
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_empty_selector() {
+ $input = '';
+ $offset = 0;
+ $result = WP_CSS_Selector::parse( $input, $offset );
+ $this->assertNull( $result );
+ }
+
/**
* @ticket TBD
*/
public function test_parse_complex_selector() {
- $input = 'el.foo#bar[baz=quux] > .child, rest';
+ $input = 'el.foo#bar[baz=quux] > .child , rest';
$offset = 0;
$sel = WP_CSS_Complex_Selector::parse( $input, $offset );
- var_dump( $sel );
$this->assertSame( 3, count( $sel->selectors ) );
$this->assertNotNull( $sel->selectors[0]->type_selector );
$this->assertSame( 3, count( $sel->selectors[0]->subclass_selectors ) );
@@ -376,4 +385,58 @@ public function test_parse_complex_selector() {
$this->assertSame( ', rest', substr( $input, $offset ) );
}
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_invalid_complex_selector() {
+ $input = 'el.foo#bar[baz=quux] > , rest';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+ $this->assertNull( $result );
+ }
+
+ public function test_parse_empty_complex_selector() {
+ $input = '';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+ $this->assertNull( $result );
+ }
+
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_selector_list() {
+ $input = 'el.foo#bar[baz=quux] .descendent , rest';
+ $result = WP_CSS_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_invalid_selector_list() {
+ $input = 'el,,';
+ $result = WP_CSS_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_invalid_selector_list2() {
+ $input = 'el!';
+ $result = WP_CSS_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_empty_selector_list() {
+ $input = " \t \t\n\r\f";
+ $result = WP_CSS_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
}