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 ) ); + } }