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 8d8ec35de98b6..8ccec5de029cc 100644
--- a/src/wp-includes/html-api/class-wp-css-selectors.php
+++ b/src/wp-includes/html-api/class-wp-css-selectors.php
@@ -123,9 +123,9 @@ private static function parse( string $input ) {
$offset = 0;
while ( $offset < $length ) {
- $sel = WP_CSS_ID_Selector::parse( $input, $offset );
- if ( $sel ) {
- $selectors[] = $sel;
+ $selector = WP_CSS_ID_Selector::parse( $input, $offset );
+ if ( null !== $selector ) {
+ $selectors[] = $selector;
}
}
if ( count( $selectors ) ) {
@@ -841,6 +841,8 @@ public static function parse( string $input, int &$offset ): ?self {
/**
* This corresponds to in the grammar.
+ *
+ * > = [ ? * ]!
*/
final class WP_CSS_Selector extends WP_CSS_Selector_Parser {
@@ -856,12 +858,7 @@ private function __construct( ?WP_CSS_Type_Selector $type_selector, array $subcl
}
/**
- * Parses a selector string into a `WP_CSS_Selector` object.
- *
* > = [ ? * ]!
- *
- * @param string $input The selector string to parse.
- * @return WP_CSS_Selector|null The parsed selector, or `null` if the selector is invalid or unsupported.
*/
public static function parse( string $input, int &$offset ): ?self {
if ( $offset >= strlen( $input ) ) {
@@ -882,6 +879,7 @@ public static function parse( string $input, int &$offset ): ?self {
$offset = $updated_offset;
return new self( $type_selector, $subclass_selectors );
}
+ return null;
}
/**
@@ -902,3 +900,76 @@ private static function parse_subclass_selector( string $input, int &$offset ) {
null ) );
}
}
+
+
+/**
+ * This corresponds to in the grammar.
+ *
+ * > = [ ? ]*
+ */
+final class WP_CSS_Complex_Selector extends WP_CSS_Selector_Parser {
+ const COMBINATOR_CHILD = '>';
+ const COMBINATOR_DESCENDANT = ' ';
+ const COMBINATOR_NEXT_SIBLING = '+';
+ const COMBINATOR_SUBSEQUENT_SIBLING = '~';
+
+ /**
+ * even indexes are WP_CSS_Selector, odd indexes are string combinators.
+ * @var array
+ */
+ public $selectors = array();
+
+ private function __construct( array $selectors ) {
+ $this->selectors = $selectors;
+ }
+
+ public static function parse( string $input, int &$offset ): ?self {
+ if ( $offset >= strlen( $input ) ) {
+ return null;
+ }
+
+ $updated_offset = $offset;
+ $selector = WP_CSS_Selector::parse( $input, $updated_offset );
+ if ( null === $selector ) {
+ return null;
+ }
+
+ $selectors = array( $selector );
+
+ $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:
+ $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;
+ break;
+ }
+ // 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;
+ return new self( $selectors );
+ }
+}
diff --git a/tests/phpunit/tests/html-api/wpCssSelectors.php b/tests/phpunit/tests/html-api/wpCssSelectors.php
index 180bee4f53c05..4189ec586011a 100644
--- a/tests/phpunit/tests/html-api/wpCssSelectors.php
+++ b/tests/phpunit/tests/html-api/wpCssSelectors.php
@@ -347,13 +347,33 @@ public function test_parse_selector() {
$offset = 0;
$sel = WP_CSS_Selector::parse( $input, $offset );
- $this->assertSame( $sel->type_selector->ident, 'el' );
- $this->assertSame( count( $sel->subclass_selectors ), 3 );
- $this->assertSame( $sel->subclass_selectors[0]->ident, 'foo' );
- $this->assertSame( $sel->subclass_selectors[1]->ident, 'bar' );
- $this->assertSame( $sel->subclass_selectors[2]->name, 'baz' );
- $this->assertSame( $sel->subclass_selectors[2]->matcher, WP_CSS_Attribute_Selector::MATCH_EXACT );
- $this->assertSame( $sel->subclass_selectors[2]->value, 'quux' );
+ $this->assertSame( 'el', $sel->type_selector->ident );
+ $this->assertSame( 3, count( $sel->subclass_selectors ) );
+ $this->assertSame( 'foo', $sel->subclass_selectors[0]->ident, 'foo' );
+ $this->assertSame( 'bar', $sel->subclass_selectors[1]->ident, 'bar' );
+ $this->assertSame( 'baz', $sel->subclass_selectors[2]->name, 'baz' );
+ $this->assertSame( WP_CSS_Attribute_Selector::MATCH_EXACT, $sel->subclass_selectors[2]->matcher );
+ $this->assertSame( 'quux', $sel->subclass_selectors[2]->value );
$this->assertSame( ' > .child', substr( $input, $offset ) );
}
+
+ /**
+ * @ticket TBD
+ */
+ public function test_parse_complex_selector() {
+ $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 ) );
+ $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_CHILD, $sel->selectors[1] );
+ $this->assertNull( $sel->selectors[2]->type_selector );
+ $this->assertSame( 1, count( $sel->selectors[2]->subclass_selectors ) );
+ $this->assertSame( 'child', $sel->selectors[2]->subclass_selectors[0]->ident );
+
+ $this->assertSame( ', rest', substr( $input, $offset ) );
+ }
}