Skip to content

Commit

Permalink
Implement complex selector
Browse files Browse the repository at this point in the history
  • Loading branch information
sirreal committed Nov 28, 2024
1 parent 3037295 commit 8d92994
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 15 deletions.
87 changes: 79 additions & 8 deletions src/wp-includes/html-api/class-wp-css-selectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) {
Expand Down Expand Up @@ -841,6 +841,8 @@ public static function parse( string $input, int &$offset ): ?self {

/**
* This corresponds to <compound-selector> in the grammar.
*
* > <compound-selector> = [ <type-selector>? <subclass-selector>* ]!
*/
final class WP_CSS_Selector extends WP_CSS_Selector_Parser {

Expand All @@ -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.
*
* > <compound-selector> = [ <type-selector>? <subclass-selector>* ]!
*
* @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 ) ) {
Expand All @@ -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;
}

/**
Expand All @@ -902,3 +900,76 @@ private static function parse_subclass_selector( string $input, int &$offset ) {
null ) );
}
}


/**
* This corresponds to <complex-selector> in the grammar.
*
* > <complex-selector> = <compound-selector> [ <combinator>? <compound-selector> ]*
*/
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<WP_CSS_Selector>
*/
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 );
}
}
34 changes: 27 additions & 7 deletions tests/phpunit/tests/html-api/wpCssSelectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );
}
}

0 comments on commit 8d92994

Please sign in to comment.