diff --git a/src/wp-includes/html-api/class-wp-css-attribute-selector.php b/src/wp-includes/html-api/class-wp-css-attribute-selector.php index be7332c85b72d..76ccdf3804b36 100644 --- a/src/wp-includes/html-api/class-wp-css-attribute-selector.php +++ b/src/wp-includes/html-api/class-wp-css-attribute-selector.php @@ -1,7 +1,9 @@ get_attribute( $this->name ); if ( null === $att_value ) { return false; @@ -76,17 +78,17 @@ public function matches( WP_HTML_Processor $processor ): bool { * @return Generator */ private function whitespace_delimited_list( string $input ): Generator { - $offset = strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS ); + $offset = strspn( $input, self::WHITESPACE_CHARACTERS ); while ( $offset < strlen( $input ) ) { // Find the byte length until the next boundary. - $length = strcspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset ); + $length = strcspn( $input, self::WHITESPACE_CHARACTERS, $offset ); if ( 0 === $length ) { return; } $value = substr( $input, $offset, $length ); - $offset += $length + strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset + $length ); + $offset += $length + strspn( $input, self::WHITESPACE_CHARACTERS, $offset + $length ); yield $value; } diff --git a/src/wp-includes/html-api/class-wp-css-class-selector.php b/src/wp-includes/html-api/class-wp-css-class-selector.php index c4f858d4a05d9..c3e7ced008a6e 100644 --- a/src/wp-includes/html-api/class-wp-css-class-selector.php +++ b/src/wp-includes/html-api/class-wp-css-class-selector.php @@ -1,7 +1,7 @@ has_class( $this->ident ); } diff --git a/src/wp-includes/html-api/class-wp-css-complex-selector-list.php b/src/wp-includes/html-api/class-wp-css-complex-selector-list.php new file mode 100644 index 0000000000000..f3769a035f6e5 --- /dev/null +++ b/src/wp-includes/html-api/class-wp-css-complex-selector-list.php @@ -0,0 +1,165 @@ + in the grammar. See {@see WP_CSS_Compound_Selector_List} for more details on the grammar. + * + * This class supports the same selector syntax as {@see WP_CSS_Compound_Selector_List} as well as: + * - The following combinators: + * - Next sibling (`el + el`) + * - Subsequent sibling (`el ~ el`) + * + * @since TBD + * + * @access private + */ +class WP_CSS_Complex_Selector_List extends WP_CSS_Compound_Selector_List implements WP_CSS_HTML_Processor_Matcher { + /** + * Takes a CSS selector string and returns an instance of itself or `null` if the selector + * string is invalid or unsupported. + * + * @since TBD + * + * @param string $input CSS selectors. + * @return static|null + */ + public static function from_selectors( string $input ) { + // > A selector string is a list of one or more complex selectors ([SELECTORS4], section 3.1) that may be surrounded by whitespace… + $input = trim( $input, " \t\r\n\r" ); + + if ( '' === $input ) { + return null; + } + + /* + * > The input stream consists of the filtered code points pushed into it as the input byte stream is decoded. + * > + * > To filter code points from a stream of (unfiltered) code points input: + * > Replace any U+000D CARRIAGE RETURN (CR) code points, U+000C FORM FEED (FF) code points, or pairs of U+000D CARRIAGE RETURN (CR) followed by U+000A LINE FEED (LF) in input by a single U+000A LINE FEED (LF) code point. + * > Replace any U+0000 NULL or surrogate code points in input with U+FFFD REPLACEMENT CHARACTER (�). + * + * https://www.w3.org/TR/css-syntax-3/#input-preprocessing + */ + $input = str_replace( array( "\r\n" ), "\n", $input ); + $input = str_replace( array( "\r", "\f" ), "\n", $input ); + $input = str_replace( "\0", "\u{FFFD}", $input ); + + $offset = 0; + + $selector = self::parse_complex_selector( $input, $offset ); + if ( null === $selector ) { + return null; + } + self::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; + self::parse_whitespace( $input, $offset ); + $selector = self::parse_complex_selector( $input, $offset ); + if ( null === $selector ) { + return null; + } + $selectors[] = $selector; + self::parse_whitespace( $input, $offset ); + } + + return new self( $selectors ); + } + + /* + * ------------------------------ + * Selector parsing functionality + * ------------------------------ + */ + + /** + * Parses a complex selector. + * + * > = [ ? ]* + * + * @return WP_CSS_Complex_Selector|null + */ + final protected static function parse_complex_selector( string $input, int &$offset ): ?WP_CSS_Complex_Selector { + if ( $offset >= strlen( $input ) ) { + return null; + } + + $updated_offset = $offset; + $selector = self::parse_compound_selector( $input, $updated_offset ); + if ( null === $selector ) { + return null; + } + + $selectors = array( $selector ); + $has_preceding_subclass_selector = null !== $selector->subclass_selectors; + + $found_whitespace = self::parse_whitespace( $input, $updated_offset ); + while ( $updated_offset < strlen( $input ) ) { + if ( + WP_CSS_Complex_Selector::COMBINATOR_CHILD === $input[ $updated_offset ] || + WP_CSS_Complex_Selector::COMBINATOR_NEXT_SIBLING === $input[ $updated_offset ] || + WP_CSS_Complex_Selector::COMBINATOR_SUBSEQUENT_SIBLING === $input[ $updated_offset ] + ) { + $combinator = $input[ $updated_offset ]; + ++$updated_offset; + self::parse_whitespace( $input, $updated_offset ); + + // Failure to find a selector here is a parse error + $selector = self::parse_compound_selector( $input, $updated_offset ); + } elseif ( $found_whitespace ) { + /* + * Whitespace is ambiguous, it could be a descendant combinator or + * insignificant whitespace. + */ + $selector = self::parse_compound_selector( $input, $updated_offset ); + if ( null === $selector ) { + break; + } + $combinator = WP_CSS_Complex_Selector::COMBINATOR_DESCENDANT; + } else { + break; + } + + if ( null === $selector ) { + return null; + } + + // `div > .className` is valid, but `.className > div` is not. + if ( $has_preceding_subclass_selector ) { + throw new Exception( 'Unsupported non-final subclass selector.' ); + } + $has_preceding_subclass_selector = null !== $selector->subclass_selectors; + + $selectors[] = $combinator; + $selectors[] = $selector; + + $found_whitespace = self::parse_whitespace( $input, $updated_offset ); + } + $offset = $updated_offset; + return new WP_CSS_Complex_Selector( $selectors ); + } +} diff --git a/src/wp-includes/html-api/class-wp-css-selector.php b/src/wp-includes/html-api/class-wp-css-compound-selector-list.php similarity index 87% rename from src/wp-includes/html-api/class-wp-css-selector.php rename to src/wp-includes/html-api/class-wp-css-compound-selector-list.php index 487c100ab47e4..2aae51d671f6b 100644 --- a/src/wp-includes/html-api/class-wp-css-selector.php +++ b/src/wp-includes/html-api/class-wp-css-compound-selector-list.php @@ -1,6 +1,6 @@ in the grammar. The supported grammar is: + * This class is analogous to in the grammar. The supported grammar is: * * = * = # @@ -38,6 +40,10 @@ * * @link https://www.w3.org/TR/selectors/#grammar Refer to the grammar for more details. * + * This class of selectors does not support "complex" selectors. That is any selector with a + * combinator such as descendent (`.ancestor .descendant`) or child (`.parent > .child`). + * See {@see WP_CSS_Complex_Selector_List} for support of some combinators. + * * Note that this grammar has been adapted and does not support the full CSS selector grammar. * Supported selector syntax: * - Type selectors (tag names, e.g. `div`) @@ -50,12 +56,10 @@ * - child (`el > .child`) * * Unsupported selector syntax: - * - Pseudo-element selectors (e.g. `::before`) - * - Pseudo-class selectors (e.g. `:hover` or `:nth-child(2)`) - * - Namespace prefixes (e.g. `svg|title` or `[xlink|href]`) - * - The following combinators: - * - Next sibling (`el + el`) - * - Subsequent sibling (`el ~ el`) + * - Pseudo-element selectors (`::before`) + * - Pseudo-class selectors (`:hover` or `:nth-child(2)`) + * - Namespace prefixes (`svg|title` or `[xlink|href]`) + * - No combinators are supported (descendant, child, next sibling, subsequent sibling) * * Future ideas: * - Namespace type selectors could be implemented with select namespaces in order to @@ -72,8 +76,12 @@ * @link https://www.w3.org/TR/selectors-api2/ * @link https://www.w3.org/TR/selectors-4/ */ -class WP_CSS_Selector implements WP_CSS_HTML_Processor_Matcher { - public function matches( WP_HTML_Processor $processor ): bool { +class WP_CSS_Compound_Selector_List implements WP_CSS_HTML_Tag_Processor_Matcher { + /** + * @param WP_HTML_Tag_Processor $processor + * @return bool + */ + public function matches( $processor ): bool { if ( $processor->get_token_type() !== '#tag' ) { return false; } @@ -87,14 +95,16 @@ public function matches( WP_HTML_Processor $processor ): bool { } /** - * @var array + * Array of selectors. + * + * @var array */ private $selectors; /** * Constructor. * - * @param array $selectors + * @param array $selectors Array of selectors. */ protected function __construct( array $selectors ) { $this->selectors = $selectors; @@ -107,10 +117,9 @@ protected function __construct( array $selectors ) { * @since TBD * * @param string $input CSS selectors. - * @return self|null + * @return static|null */ - public static function from_selectors( string $input ): ?self { - // > A selector string is a list of one or more complex selectors ([SELECTORS4], section 3.1) that may be surrounded by whitespace… + public static function from_selectors( string $input ) { $input = trim( $input, " \t\r\n\r" ); if ( '' === $input ) { @@ -132,7 +141,7 @@ public static function from_selectors( string $input ): ?self { $offset = 0; - $selector = self::parse_complex_selector( $input, $offset ); + $selector = self::parse_compound_selector( $input, $offset ); if ( null === $selector ) { return null; } @@ -146,7 +155,7 @@ public static function from_selectors( string $input ): ?self { } ++$offset; self::parse_whitespace( $input, $offset ); - $selector = self::parse_complex_selector( $input, $offset ); + $selector = self::parse_compound_selector( $input, $offset ); if ( null === $selector ) { return null; } @@ -391,73 +400,6 @@ final protected static function parse_compound_selector( string $input, int &$of return null; } - /** - * Parses a complex selector. - * - * > = [ ? ]* - * - * @return WP_CSS_Complex_Selector|null - */ - final protected static function parse_complex_selector( string $input, int &$offset ): ?WP_CSS_Complex_Selector { - if ( $offset >= strlen( $input ) ) { - return null; - } - - $updated_offset = $offset; - $selector = self::parse_compound_selector( $input, $updated_offset ); - if ( null === $selector ) { - return null; - } - - $selectors = array( $selector ); - $has_preceding_subclass_selector = null !== $selector->subclass_selectors; - - $found_whitespace = self::parse_whitespace( $input, $updated_offset ); - while ( $updated_offset < strlen( $input ) ) { - if ( - WP_CSS_Complex_Selector::COMBINATOR_CHILD === $input[ $updated_offset ] || - WP_CSS_Complex_Selector::COMBINATOR_NEXT_SIBLING === $input[ $updated_offset ] || - WP_CSS_Complex_Selector::COMBINATOR_SUBSEQUENT_SIBLING === $input[ $updated_offset ] - ) { - $combinator = $input[ $updated_offset ]; - ++$updated_offset; - self::parse_whitespace( $input, $updated_offset ); - - // Failure to find a selector here is a parse error - $selector = self::parse_compound_selector( $input, $updated_offset ); - } elseif ( $found_whitespace ) { - /* - * Whitespace is ambiguous, it could be a descendant combinator or - * insignificant whitespace. - */ - $selector = self::parse_compound_selector( $input, $updated_offset ); - if ( null === $selector ) { - break; - } - $combinator = WP_CSS_Complex_Selector::COMBINATOR_DESCENDANT; - } else { - break; - } - - if ( null === $selector ) { - return null; - } - - // `div > .className` is valid, but `.className > div` is not. - if ( $has_preceding_subclass_selector ) { - throw new Exception( 'Unsupported non-final subclass selector.' ); - } - $has_preceding_subclass_selector = null !== $selector->subclass_selectors; - - $selectors[] = $combinator; - $selectors[] = $selector; - - $found_whitespace = self::parse_whitespace( $input, $updated_offset ); - } - $offset = $updated_offset; - return new WP_CSS_Complex_Selector( $selectors ); - } - /** * Parses a subclass selector. * @@ -496,7 +438,7 @@ private static function parse_subclass_selector( string $input, int &$offset ) { const UTF8_MAX_CODEPOINT_VALUE = 0x10FFFF; const WHITESPACE_CHARACTERS = " \t\r\n\f"; - public static function parse_whitespace( string $input, int &$offset ): bool { + final public static function parse_whitespace( string $input, int &$offset ): bool { $length = strspn( $input, self::WHITESPACE_CHARACTERS, $offset ); $advanced = $length > 0; $offset += $length; @@ -692,9 +634,9 @@ final protected static function parse_string( string $input, int &$offset ): ?st * * @param string $input * @param int $offset - * @return string|null + * @return string */ - final protected static function consume_escaped_codepoint( $input, &$offset ): ?string { + final protected static function consume_escaped_codepoint( $input, &$offset ): string { $hex_length = strspn( $input, '0123456789abcdefABCDEF', $offset, 6 ); if ( $hex_length > 0 ) { /** @@ -771,7 +713,7 @@ final protected static function consume_escaped_codepoint( $input, &$offset ): ? * @param int $offset The byte offset in the string. * @return bool True if the next two codepoints are a valid escape, otherwise false. */ - private static function next_two_are_valid_escape( string $input, int $offset ): bool { + final protected static function next_two_are_valid_escape( string $input, int $offset ): bool { if ( $offset + 1 >= strlen( $input ) ) { return false; } @@ -858,7 +800,7 @@ final protected static function is_ident_codepoint( string $input, int $offset ) * @param int $offset The byte offset in the string. * @return bool True if the next three codepoints would start an ident sequence, otherwise false. */ - private static function check_if_three_code_points_would_start_an_ident_sequence( string $input, int $offset ): bool { + final protected static function check_if_three_code_points_would_start_an_ident_sequence( string $input, int $offset ): bool { if ( $offset >= strlen( $input ) ) { return false; } diff --git a/src/wp-includes/html-api/class-wp-css-compound-selector.php b/src/wp-includes/html-api/class-wp-css-compound-selector.php index 1162aaef78c1e..e64695abe9ab3 100644 --- a/src/wp-includes/html-api/class-wp-css-compound-selector.php +++ b/src/wp-includes/html-api/class-wp-css-compound-selector.php @@ -5,8 +5,8 @@ * * > = [ ? * ]! */ -final class WP_CSS_Compound_Selector implements WP_CSS_HTML_Processor_Matcher { - public function matches( WP_HTML_Processor $processor ): bool { +final class WP_CSS_Compound_Selector implements WP_CSS_HTML_Tag_Processor_Matcher { + public function matches( WP_HTML_Tag_Processor $processor ): bool { if ( $this->type_selector ) { if ( ! $this->type_selector->matches( $processor ) ) { return false; diff --git a/src/wp-includes/html-api/class-wp-css-id-selector.php b/src/wp-includes/html-api/class-wp-css-id-selector.php index cc0589327c829..83339ff839317 100644 --- a/src/wp-includes/html-api/class-wp-css-id-selector.php +++ b/src/wp-includes/html-api/class-wp-css-id-selector.php @@ -1,6 +1,6 @@ ident = $ident; } - public function matches( WP_HTML_Processor $processor ): bool { + public function matches( WP_HTML_Tag_Processor $processor ): bool { $id = $processor->get_attribute( 'id' ); if ( ! is_string( $id ) ) { return false; } - $case_insensitive = method_exists( $processor, 'is_quirks_mode' ) && $processor->is_quirks_mode(); + $case_insensitive = $processor->is_quirks_mode(); + return $case_insensitive ? 0 === strcasecmp( $id, $this->ident ) : $processor->get_attribute( 'id' ) === $this->ident; diff --git a/src/wp-includes/html-api/class-wp-css-type-selector.php b/src/wp-includes/html-api/class-wp-css-type-selector.php index a2dcd16521cb5..c65adce14047d 100644 --- a/src/wp-includes/html-api/class-wp-css-type-selector.php +++ b/src/wp-includes/html-api/class-wp-css-type-selector.php @@ -1,7 +1,7 @@ get_tag(); if ( null === $tag_name ) { return false; diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 9f7a43acaebbd..bbca730279876 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -657,9 +657,14 @@ public function get_unsupported_exception() { * @param string $selector_string Selector string. * @return Generator A generator pausing on each tag matching the selector. */ - public function select_all( string $selector_string ): Generator { - $selector = WP_CSS_Selector::from_selectors( $selector_string ); + public function select_all( $selector_string ): Generator { + $selector = WP_CSS_Complex_Selector_List::from_selectors( $selector_string ); if ( null === $selector ) { + _doing_it_wrong( + __METHOD__, + sprintf( 'Received unsupported or invalid selector "%s".', $selector_string ), + '6.8' + ); return; } @@ -692,7 +697,7 @@ public function select_all( string $selector_string ): Generator { * @param string $selector_string * @return bool True if a matching tag was found, otherwise false. */ - public function select( string $selector_string ) { + public function select( string $selector_string ): bool { foreach ( $this->select_all( $selector_string ) as $_ ) { return true; } diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 7dadbc1bebdb2..a7633291b6bb2 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -860,6 +860,75 @@ public function change_parsing_namespace( string $new_namespace ): bool { return true; } + /** + * Progress through a document pausing on tags matching the provided CSS selector string. + * + * @example + * + * $processor = new WP_HTML_Tag_Processor( + * 'Example' + * ); + * foreach ( $processor->select_all( 'meta[property^="og:" i]' ) as $_ ) { + * // Loop is entered twice. + * var_dump( + * $processor->get_tag(), // string(4) "META" + * $processor->get_attribute( 'property' ), // string(7) "og:type" / string(14) "og:description" + * $processor->get_attribute( 'content' ), // string(7) "website" / string(11) "An example." + * ); + * } + * + * @since TBD + * + * @param string $selector_string Selector string. + * @return Generator A generator pausing on each tag matching the selector. + */ + public function select_all( $selector_string ): Generator { + $selector = WP_CSS_Compound_Selector_List::from_selectors( $selector_string ); + if ( null === $selector ) { + _doing_it_wrong( + __METHOD__, + sprintf( 'Received unsupported or invalid selector "%s".', $selector_string ), + '6.8' + ); + return; + } + + while ( $this->next_tag() ) { + if ( $selector->matches( $this ) ) { + yield; + } + } + } + + /** + * Move to the next tag matching the provided CSS selector string. + * + * This method will stop at the next match. To progress through all matches, use + * the {@see WP_HTML_Tag_Processor::select_all()} method. + * + * @example + * + * $processor = new WP_HTML_Tag_Processor( + * 'Example' + * ); + * $processor->select( 'meta[charset]' ); + * var_dump( + * $processor->get_tag(), // string(4) "META" + * $processor->get_attribute( 'charset' ), // string(5) "utf-8" + * ); + * + * @since TBD + * + * @param string $selector_string + * @return bool True if a matching tag was found, otherwise false. + */ + public function select( string $selector_string ): bool { + foreach ( $this->select_all( $selector_string ) as $_ ) { + return true; + } + return false; + } + /** * Finds the next tag matching the $query. * diff --git a/src/wp-includes/html-api/interface-wp-css-html-tag-processor-matcher.php b/src/wp-includes/html-api/interface-wp-css-html-tag-processor-matcher.php new file mode 100644 index 0000000000000..73d108150bb95 --- /dev/null +++ b/src/wp-includes/html-api/interface-wp-css-html-tag-processor-matcher.php @@ -0,0 +1,8 @@ +test_class = new class() extends WP_CSS_Complex_Selector_List { + public function __construct() { + parent::__construct( array() ); + } + + public static function test_parse_complex_selector( string $input, int &$offset ) { + return self::parse_complex_selector( $input, $offset ); + } + }; + } + + /** + * @ticket TBD + */ + public function test_parse_complex_selector() { + $input = 'el1 > .child#bar[baz=quux] , rest'; + $offset = 0; + $sel = $this->test_class::test_parse_complex_selector( $input, $offset ); + + $this->assertSame( 3, count( $sel->selectors ) ); + + $this->assertSame( 'el1', $sel->selectors[2]->type_selector->ident ); + $this->assertNull( $sel->selectors[2]->subclass_selectors ); + + $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_CHILD, $sel->selectors[1] ); + + $this->assertSame( 3, count( $sel->selectors[0]->subclass_selectors ) ); + $this->assertNull( $sel->selectors[0]->type_selector ); + $this->assertSame( 3, count( $sel->selectors[0]->subclass_selectors ) ); + $this->assertSame( 'child', $sel->selectors[0]->subclass_selectors[0]->ident ); + + $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 = $this->test_class::test_parse_complex_selector( $input, $offset ); + $this->assertNull( $result ); + } + + /** + * @ticket TBD + */ + public function test_parse_empty_complex_selector() { + $input = ''; + $offset = 0; + $result = $this->test_class::test_parse_complex_selector( $input, $offset ); + $this->assertNull( $result ); + } + + /** + * @ticket TBD + */ + public function test_parse_complex_selector_list() { + $input = 'el1 el2 el.foo#bar[baz=quux], second > selector'; + $result = WP_CSS_Complex_Selector_List::from_selectors( $input ); + $this->assertNotNull( $result ); + } + + /** + * @ticket TBD + */ + public function test_parse_invalid_selector_list() { + $input = 'el,,'; + $result = WP_CSS_Complex_Selector_List::from_selectors( $input ); + $this->assertNull( $result ); + } + + /** + * @ticket TBD + */ + public function test_parse_invalid_selector_list2() { + $input = 'el!'; + $result = WP_CSS_Complex_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_Complex_Selector_List::from_selectors( $input ); + $this->assertNull( $result ); + } +} diff --git a/tests/phpunit/tests/html-api/wpCssSelector-parsing.php b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php similarity index 89% rename from tests/phpunit/tests/html-api/wpCssSelector-parsing.php rename to tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php index 4caa186158149..d94b61d49c14e 100644 --- a/tests/phpunit/tests/html-api/wpCssSelector-parsing.php +++ b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php @@ -10,12 +10,12 @@ * * @group html-api */ -class Tests_HtmlApi_WpCssSelector_Parsing extends WP_UnitTestCase { +class Tests_HtmlApi_WpCssCompoundSelectorList extends WP_UnitTestCase { private $test_class; public function set_up(): void { parent::set_up(); - $this->test_class = new class() extends WP_CSS_Selector { + $this->test_class = new class() extends WP_CSS_Compound_Selector_List { public function __construct() { parent::__construct( array() ); } @@ -51,10 +51,6 @@ public static function test_parse_compound_selector( string $input, int &$offset return self::parse_compound_selector( $input, $offset ); } - public static function test_parse_complex_selector( string $input, int &$offset ) { - return self::parse_complex_selector( $input, $offset ); - } - /* * Utilities */ @@ -402,53 +398,12 @@ public function test_parse_empty_selector() { $this->assertSame( 0, $offset ); } - /** - * @ticket TBD - */ - public function test_parse_complex_selector() { - $input = 'el1 > .child#bar[baz=quux] , rest'; - $offset = 0; - $sel = $this->test_class::test_parse_complex_selector( $input, $offset ); - - $this->assertSame( 3, count( $sel->selectors ) ); - - $this->assertSame( 'el1', $sel->selectors[2]->type_selector->ident ); - $this->assertNull( $sel->selectors[2]->subclass_selectors ); - - $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_CHILD, $sel->selectors[1] ); - - $this->assertSame( 3, count( $sel->selectors[0]->subclass_selectors ) ); - $this->assertNull( $sel->selectors[0]->type_selector ); - $this->assertSame( 3, count( $sel->selectors[0]->subclass_selectors ) ); - $this->assertSame( 'child', $sel->selectors[0]->subclass_selectors[0]->ident ); - - $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 = $this->test_class::test_parse_complex_selector( $input, $offset ); - $this->assertNull( $result ); - } - - public function test_parse_empty_complex_selector() { - $input = ''; - $offset = 0; - $result = $this->test_class::test_parse_complex_selector( $input, $offset ); - $this->assertNull( $result ); - } - - /** * @ticket TBD */ public function test_parse_selector_list() { - $input = 'el1 el2 el.foo#bar[baz=quux], rest'; - $result = WP_CSS_Selector::from_selectors( $input ); + $input = 'el1, el2, el.foo#bar[baz=quux]'; + $result = WP_CSS_Compound_Selector_List::from_selectors( $input ); $this->assertNotNull( $result ); } @@ -457,7 +412,7 @@ public function test_parse_selector_list() { */ public function test_parse_invalid_selector_list() { $input = 'el,,'; - $result = WP_CSS_Selector::from_selectors( $input ); + $result = WP_CSS_Compound_Selector_List::from_selectors( $input ); $this->assertNull( $result ); } @@ -466,7 +421,7 @@ public function test_parse_invalid_selector_list() { */ public function test_parse_invalid_selector_list2() { $input = 'el!'; - $result = WP_CSS_Selector::from_selectors( $input ); + $result = WP_CSS_Compound_Selector_List::from_selectors( $input ); $this->assertNull( $result ); } @@ -475,7 +430,7 @@ public function test_parse_invalid_selector_list2() { */ public function test_parse_empty_selector_list() { $input = " \t \t\n\r\f"; - $result = WP_CSS_Selector::from_selectors( $input ); + $result = WP_CSS_Compound_Selector_List::from_selectors( $input ); $this->assertNull( $result ); } } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-select.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-select.php index c3a1e4121ecab..733a7135f1b17 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-select.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-select.php @@ -66,4 +66,14 @@ public function test_select_all() { } $this->assertSame( 4, $count ); } + + /** + * @ticket TBD + * + * @expectedIncorrectUsage WP_HTML_Processor::select_all + */ + public function test_invalid_selector() { + $processor = WP_HTML_Processor::create_fragment( 'irrelevant' ); + $this->assertFalse( $processor->select( '[invalid!selector]' ) ); + } } diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor-select.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor-select.php new file mode 100644 index 0000000000000..c42c69ff0a095 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor-select.php @@ -0,0 +1,92 @@ +' ); + $this->assertFalse( $processor->select( 'div' ) ); + } + + /** + * @ticket TBD + * + * @dataProvider data_selectors + */ + public function test_select( string $html, string $selector ) { + $processor = new WP_HTML_Tag_Processor( $html ); + $this->assertTrue( $processor->select( $selector ) ); + $this->assertTrue( $processor->get_attribute( 'match' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_selectors(): array { + return array( + 'simple type' => array( '
', 'div' ), + 'any type' => array( '', '*' ), + 'simple class' => array( '
', '.x' ), + 'simple id' => array( '
', '#x' ), + 'simple attribute' => array( '
', '[att]' ), + 'attribute value' => array( '
', '[att=val]' ), + 'attribute quoted value' => array( '
', '[att="::"]' ), + + 'list' => array( '

', 'a, p' ), + 'compound' => array( '

', 'section[att~="bar"]' ), + ); + } + + /** + * @ticket TBD + */ + public function test_select_all() { + $processor = new WP_HTML_Tag_Processor( '

' ); + $count = 0; + foreach ( $processor->select_all( 'div, .x, rect, #y' ) as $_ ) { + ++$count; + $this->assertTrue( $processor->get_attribute( 'match' ) ); + } + $this->assertSame( 4, $count ); + } + + /** + * @ticket TBD + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::select_all + * + * @dataProvider data_invalid_selectors + */ + public function test_invalid_selector( string $selector ) { + $processor = new WP_HTML_Tag_Processor( 'irrelevant' ); + $this->assertFalse( $processor->select( $selector ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_invalid_selectors(): array { + return array( + 'complex descendant' => array( 'div *' ), + 'complex child' => array( 'div > *' ), + 'invalid selector' => array( '[invalid!selector]' ), + ); + } +}