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/class-wp-html-text-replacement.php b/src/wp-includes/html-api/class-wp-html-text-replacement.php
index 65e17d48fdb4a..797c101aead2d 100644
--- a/src/wp-includes/html-api/class-wp-html-text-replacement.php
+++ b/src/wp-includes/html-api/class-wp-html-text-replacement.php
@@ -18,6 +18,7 @@
* @since 6.5.0 Replace `end` with `length` to more closely match `substr()`.
*
* @see WP_HTML_Tag_Processor
+ * @codeCoverageIgnore
*/
class WP_HTML_Text_Replacement {
/**
diff --git a/src/wp-includes/html-api/class-wp-html-token.php b/src/wp-includes/html-api/class-wp-html-token.php
index d5e51ac29007f..55645644c7ba3 100644
--- a/src/wp-includes/html-api/class-wp-html-token.php
+++ b/src/wp-includes/html-api/class-wp-html-token.php
@@ -17,6 +17,8 @@
*
* @access private
*
+ * @codeCoverageIgnore
+ *
* @see WP_HTML_Processor
*/
class WP_HTML_Token {
diff --git a/src/wp-includes/html-api/class-wp-html-unsupported-exception.php b/src/wp-includes/html-api/class-wp-html-unsupported-exception.php
index 7b244a5e8a8dd..ce254225ed967 100644
--- a/src/wp-includes/html-api/class-wp-html-unsupported-exception.php
+++ b/src/wp-includes/html-api/class-wp-html-unsupported-exception.php
@@ -25,6 +25,7 @@
*
* @access private
*
+ * @codeCoverageIgnore
* @see WP_HTML_Processor
*/
class WP_HTML_Unsupported_Exception extends Exception {
diff --git a/src/wp-includes/html-api/html5-named-character-references.php b/src/wp-includes/html-api/html5-named-character-references.php
index 9466f0a06b8cb..1094a52ead1d3 100644
--- a/src/wp-includes/html-api/html5-named-character-references.php
+++ b/src/wp-includes/html-api/html5-named-character-references.php
@@ -14,6 +14,7 @@
*
* @package WordPress
* @since 6.6.0
+ * @codeCoverageIgnore
*/
// phpcs:disable
diff --git a/src/wp-includes/html-api/interface-wp-css-html-processor-matcher.php b/src/wp-includes/html-api/interface-wp-css-html-processor-matcher.php
new file mode 100644
index 0000000000000..2ae29413b35d2
--- /dev/null
+++ b/src/wp-includes/html-api/interface-wp-css-html-processor-matcher.php
@@ -0,0 +1,8 @@
+initialize();
}
+// To make get_plugin_data() available in a way that's compatible with plugins also loading this file, see #62244.
+require_once ABSPATH . 'wp-admin/includes/plugin.php';
+
// Load active plugins.
foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
wp_register_plugin_realpath( $plugin );
diff --git a/tests/phpunit/tests/blocks/updateIgnoredHookedBlocksPostMeta.php b/tests/phpunit/tests/blocks/updateIgnoredHookedBlocksPostMeta.php
index 7b0a830dd52dd..18b5eff08ba79 100644
--- a/tests/phpunit/tests/blocks/updateIgnoredHookedBlocksPostMeta.php
+++ b/tests/phpunit/tests/blocks/updateIgnoredHookedBlocksPostMeta.php
@@ -193,4 +193,31 @@ public function test_update_ignored_hooked_blocks_postmeta_dont_modify_if_no_pos
'Post content did not match the original markup.'
);
}
+
+ /**
+ * @ticket 62639
+ */
+ public function test_update_ignored_hooked_blocks_postmeta_sets_correct_context_type() {
+ $action = new MockAction();
+ add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 );
+
+ $original_markup = '';
+ $post = new stdClass();
+ $post->ID = self::$navigation_post->ID;
+ $post->post_content = $original_markup;
+ $post->post_type = 'wp_navigation';
+
+ $post = update_ignored_hooked_blocks_postmeta( $post );
+
+ $args = $action->get_args();
+ $contexts = array_column( $args, 3 );
+
+ foreach ( $contexts as $context ) {
+ $this->assertInstanceOf(
+ WP_Post::class,
+ $context,
+ 'The context passed to the hooked_block_types filter is not a WP_Post instance.'
+ );
+ }
+ }
}
diff --git a/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php b/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
new file mode 100644
index 0000000000000..0b17e57847662
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
@@ -0,0 +1,117 @@
+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 62653
+ */
+ 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 62653
+ */
+ 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 62653
+ */
+ public function test_parse_invalid_complex_selector_nonfinal_subclass() {
+ $input = 'el.foo#bar[baz=quux] > final, rest';
+ $offset = 0;
+ $result = $this->test_class::test_parse_complex_selector( $input, $offset );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_empty_complex_selector() {
+ $input = '';
+ $offset = 0;
+ $result = $this->test_class::test_parse_complex_selector( $input, $offset );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ 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 62653
+ */
+ public function test_parse_invalid_selector_list() {
+ $input = 'el,,';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_invalid_selector_list2() {
+ $input = 'el!';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ 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/wpCssCompoundSelectorList.php b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
new file mode 100644
index 0000000000000..715e0e26bc9cd
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
@@ -0,0 +1,440 @@
+test_class = new class() extends WP_CSS_Compound_Selector_List {
+ public function __construct() {
+ parent::__construct( array() );
+ }
+
+ /*
+ * Parsing
+ */
+ public static function test_parse_ident( string $input, int &$offset ) {
+ return self::parse_ident( $input, $offset );
+ }
+
+ public static function test_parse_string( string $input, int &$offset ) {
+ return self::parse_string( $input, $offset );
+ }
+
+ public static function test_parse_type_selector( string $input, int &$offset ) {
+ return self::parse_type_selector( $input, $offset );
+ }
+
+ public static function test_parse_id_selector( string $input, int &$offset ) {
+ return self::parse_id_selector( $input, $offset );
+ }
+
+ public static function test_parse_class_selector( string $input, int &$offset ) {
+ return self::parse_class_selector( $input, $offset );
+ }
+
+ public static function test_parse_attribute_selector( string $input, int &$offset ) {
+ return self::parse_attribute_selector( $input, $offset );
+ }
+
+ public static function test_parse_compound_selector( string $input, int &$offset ) {
+ return self::parse_compound_selector( $input, $offset );
+ }
+
+ /*
+ * Utilities
+ */
+ public static function test_is_ident_codepoint( string $input, int $offset ) {
+ return self::is_ident_codepoint( $input, $offset );
+ }
+
+ public static function test_is_ident_start_codepoint( string $input, int $offset ) {
+ return self::is_ident_start_codepoint( $input, $offset );
+ }
+ };
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_idents(): array {
+ return array(
+ 'trailing #' => array( '_-foo123#xyz', '_-foo123', '#xyz' ),
+ 'trailing .' => array( '😍foo123.xyz', '😍foo123', '.xyz' ),
+ 'trailing " "' => array( '😍foo123 more', '😍foo123', ' more' ),
+ 'escaped ASCII character' => array( '\\xyz', 'xyz', '' ),
+ 'escaped space' => array( '\\ x', ' x', '' ),
+ 'escaped emoji' => array( '\\😍', '😍', '' ),
+ 'hex unicode codepoint' => array( '\\1f0a1', '🂡', '' ),
+ 'HEX UNICODE CODEPOINT' => array( '\\1D4B2', '𝒲', '' ),
+
+ 'hex tab-suffixed 1' => array( "\\31\t23", '123', '' ),
+ 'hex newline-suffixed 1' => array( "\\31\n23", '123', '' ),
+ 'hex space-suffixed 1' => array( "\\31 23", '123', '' ),
+ 'hex tab' => array( '\\9', "\t", '' ),
+ 'hex a' => array( '\\61 bc', 'abc', '' ),
+ 'hex a max escape length' => array( '\\000061bc', 'abc', '' ),
+
+ 'out of range replacement min' => array( '\\110000 ', "\u{fffd}", '' ),
+ 'out of range replacement max' => array( '\\ffffff ', "\u{fffd}", '' ),
+ 'leading surrogate min replacement' => array( '\\d800 ', "\u{fffd}", '' ),
+ 'leading surrogate max replacement' => array( '\\dbff ', "\u{fffd}", '' ),
+ 'trailing surrogate min replacement' => array( '\\dc00 ', "\u{fffd}", '' ),
+ 'trailing surrogate max replacement' => array( '\\dfff ', "\u{fffd}", '' ),
+ 'can start with -ident' => array( '-ident', '-ident', '' ),
+ 'can start with --anything' => array( '--anything', '--anything', '' ),
+ 'can start with ---anything' => array( '--_anything', '--_anything', '' ),
+ 'can start with --1anything' => array( '--1anything', '--1anything', '' ),
+ 'can start with -\31 23' => array( '-\31 23', '-123', '' ),
+ 'can start with --\31 23' => array( '--\31 23', '--123', '' ),
+ 'ident ends before ]' => array( 'ident]', 'ident', ']' ),
+
+ // Invalid
+ 'Invalid: (empty string)' => array( '' ),
+ 'Invalid: bad start >' => array( '>ident' ),
+ 'Invalid: bad start [' => array( '[ident' ),
+ 'Invalid: bad start #' => array( '#ident' ),
+ 'Invalid: bad start " "' => array( ' ident' ),
+ 'Invalid: bad start 1' => array( '1ident' ),
+ 'Invalid: bad start -1' => array( '-1ident' ),
+ 'Invalid: bad start -' => array( '-' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_is_ident_and_is_ident_start() {
+ $this->assertFalse( $this->test_class::test_is_ident_codepoint( '[', 0 ) );
+ $this->assertFalse( $this->test_class::test_is_ident_codepoint( ']', 0 ) );
+ $this->assertFalse( $this->test_class::test_is_ident_start_codepoint( '[', 0 ) );
+ $this->assertFalse( $this->test_class::test_is_ident_start_codepoint( ']', 0 ) );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_idents
+ */
+ public function test_parse_ident( string $input, ?string $expected = null, ?string $rest = null ) {
+
+ $offset = 0;
+ $result = $this->test_class::test_parse_ident( $input, $offset );
+ if ( null === $expected ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected, $result, 'Ident did not match.' );
+ $this->assertSame( $rest, substr( $input, $offset ), 'Offset was not updated correctly.' );
+ }
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_strings
+ */
+ public function test_parse_string( string $input, ?string $expected = null, ?string $rest = null ) {
+ $offset = 0;
+ $result = $this->test_class::test_parse_string( $input, $offset );
+ if ( null === $expected ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected, $result, 'String did not match.' );
+ $this->assertSame( $rest, substr( $input, $offset ), 'Offset was not updated correctly.' );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_strings(): array {
+ return array(
+ '"foo"' => array( '"foo"', 'foo', '' ),
+ '"foo"after' => array( '"foo"after', 'foo', 'after' ),
+ '"foo""two"' => array( '"foo""two"', 'foo', '"two"' ),
+ '"foo"\'two\'' => array( '"foo"\'two\'', 'foo', "'two'" ),
+
+ "'foo'" => array( "'foo'", 'foo', '' ),
+ "'foo'after" => array( "'foo'after", 'foo', 'after' ),
+ "'foo'\"two\"" => array( "'foo'\"two\"", 'foo', '"two"' ),
+ "'foo''two'" => array( "'foo''two'", 'foo', "'two'" ),
+
+ "'foo\\nbar'" => array( "'foo\\\nbar'", 'foobar', '' ),
+ "'foo\\31 23'" => array( "'foo\\31 23'", 'foo123', '' ),
+ "'foo\\31\\n23'" => array( "'foo\\31\n23'", 'foo123', '' ),
+ "'foo\\31\\t23'" => array( "'foo\\31\t23'", 'foo123', '' ),
+ "'foo\\00003123'" => array( "'foo\\00003123'", 'foo123', '' ),
+
+ "'foo\\" => array( "'foo\\", 'foo', '' ),
+
+ '"' => array( '"', '', '' ),
+ '"\\"' => array( '"\\"', '"', '' ),
+ '"missing close' => array( '"missing close', 'missing close', '' ),
+
+ // Invalid
+ 'Invalid: (empty string)' => array( '' ),
+ 'Invalid: .foo' => array( '.foo' ),
+ 'Invalid: #foo' => array( '#foo' ),
+ "Invalid: 'newline\\n'" => array( "'newline\n'" ),
+ 'Invalid: foo' => array( 'foo' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_id_selectors
+ */
+ public function test_parse_id( string $input, ?string $expected = null, ?string $rest = null ) {
+ $offset = 0;
+ $result = $this->test_class::test_parse_id_selector( $input, $offset );
+ if ( null === $expected ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected, $result->ident );
+ $this->assertSame( $rest, substr( $input, $offset ) );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_id_selectors(): array {
+ return array(
+ 'valid #_-foo123' => array( '#_-foo123', '_-foo123', '' ),
+ 'valid #foo#bar' => array( '#foo#bar', 'foo', '#bar' ),
+ 'escaped #\31 23' => array( '#\\31 23', '123', '' ),
+ 'with descendant #\31 23 div' => array( '#\\31 23 div', '123', ' div' ),
+
+ 'not ID foo' => array( 'foo' ),
+ 'not ID .bar' => array( '.bar' ),
+ 'not valid #1foo' => array( '#1foo' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_class_selectors
+ */
+ public function test_parse_class( string $input, ?string $expected = null, ?string $rest = null ) {
+ $offset = 0;
+ $result = $this->test_class::test_parse_class_selector( $input, $offset );
+ if ( null === $expected ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected, $result->ident );
+ $this->assertSame( $rest, substr( $input, $offset ) );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_class_selectors(): array {
+ return array(
+ 'valid ._-foo123' => array( '._-foo123', '_-foo123', '' ),
+ 'valid .foo.bar' => array( '.foo.bar', 'foo', '.bar' ),
+ 'escaped .\31 23' => array( '.\\31 23', '123', '' ),
+ 'with descendant .\31 23 div' => array( '.\\31 23 div', '123', ' div' ),
+
+ 'not class foo' => array( 'foo' ),
+ 'not class #bar' => array( '#bar' ),
+ 'not valid .1foo' => array( '.1foo' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_type_selectors
+ */
+ public function test_parse_type( string $input, ?string $expected = null, ?string $rest = null ) {
+ $offset = 0;
+ $result = $this->test_class::test_parse_type_selector( $input, $offset );
+ if ( null === $expected ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected, $result->ident );
+ $this->assertSame( $rest, substr( $input, $offset ) );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_type_selectors(): array {
+ return array(
+ 'any *' => array( '* .class', '*', ' .class' ),
+ 'a' => array( 'a', 'a', '' ),
+ 'div.class' => array( 'div.class', 'div', '.class' ),
+ 'custom-type#id' => array( 'custom-type#id', 'custom-type', '#id' ),
+
+ // Invalid
+ 'Invalid: (empty string)' => array( '' ),
+ 'Invalid: #id' => array( '#id' ),
+ 'Invalid: .class' => array( '.class' ),
+ 'Invalid: [attr]' => array( '[attr]' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_attribute_selectors
+ */
+ public function test_parse_attribute(
+ string $input,
+ ?string $expected_name = null,
+ ?string $expected_matcher = null,
+ ?string $expected_value = null,
+ ?string $expected_modifier = null,
+ ?string $rest = null
+ ) {
+ $offset = 0;
+ $result = $this->test_class::test_parse_attribute_selector( $input, $offset );
+ if ( null === $expected_name ) {
+ $this->assertNull( $result );
+ } else {
+ $this->assertSame( $expected_name, $result->name );
+ $this->assertSame( $expected_matcher, $result->matcher );
+ $this->assertSame( $expected_value, $result->value );
+ $this->assertSame( $expected_modifier, $result->modifier );
+ $this->assertSame( $rest, substr( $input, $offset ) );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_attribute_selectors(): array {
+ return array(
+ '[href]' => array( '[href]', 'href', null, null, null, '' ),
+ '[href] type' => array( '[href] type', 'href', null, null, null, ' type' ),
+ '[href]#id' => array( '[href]#id', 'href', null, null, null, '#id' ),
+ '[href].class' => array( '[href].class', 'href', null, null, null, '.class' ),
+ '[href][href2]' => array( '[href][href2]', 'href', null, null, null, '[href2]' ),
+ '[\n href\t\r]' => array( "[\n href\t\r]", 'href', null, null, null, '' ),
+ '[href=foo]' => array( '[href=foo]', 'href', WP_CSS_Attribute_Selector::MATCH_EXACT, 'foo', null, '' ),
+ '[href \n = bar ]' => array( "[href \n = bar ]", 'href', WP_CSS_Attribute_Selector::MATCH_EXACT, 'bar', null, '' ),
+ '[href \n ^= baz ]' => array( "[href \n ^= baz ]", 'href', WP_CSS_Attribute_Selector::MATCH_PREFIXED_BY, 'baz', null, '' ),
+
+ '[match $= insensitive i]' => array( '[match $= insensitive i]', 'match', WP_CSS_Attribute_Selector::MATCH_SUFFIXED_BY, 'insensitive', WP_CSS_Attribute_Selector::MODIFIER_CASE_INSENSITIVE, '' ),
+ '[match|=sensitive s]' => array( '[match|=sensitive s]', 'match', WP_CSS_Attribute_Selector::MATCH_EXACT_OR_EXACT_WITH_HYPHEN, 'sensitive', WP_CSS_Attribute_Selector::MODIFIER_CASE_SENSITIVE, '' ),
+ '[att=val I]' => array( '[att=val I]', 'att', WP_CSS_Attribute_Selector::MATCH_EXACT, 'val', WP_CSS_Attribute_Selector::MODIFIER_CASE_INSENSITIVE, '' ),
+ '[att=val S]' => array( '[att=val S]', 'att', WP_CSS_Attribute_Selector::MATCH_EXACT, 'val', WP_CSS_Attribute_Selector::MODIFIER_CASE_SENSITIVE, '' ),
+
+ '[match~="quoted[][]"]' => array( '[match~="quoted[][]"]', 'match', WP_CSS_Attribute_Selector::MATCH_ONE_OF_EXACT, 'quoted[][]', null, '' ),
+ "[match$='quoted!{}']" => array( "[match$='quoted!{}']", 'match', WP_CSS_Attribute_Selector::MATCH_SUFFIXED_BY, 'quoted!{}', null, '' ),
+ "[match*='quoted's]" => array( "[match*='quoted's]", 'match', WP_CSS_Attribute_Selector::MATCH_CONTAINS, 'quoted', WP_CSS_Attribute_Selector::MODIFIER_CASE_SENSITIVE, '' ),
+
+ '[escape-nl="foo\\nbar"]' => array( "[escape-nl='foo\\\nbar']", 'escape-nl', WP_CSS_Attribute_Selector::MATCH_EXACT, 'foobar', null, '' ),
+ '[escape-seq="\\31 23"]' => array( "[escape-seq='\\31 23']", 'escape-seq', WP_CSS_Attribute_Selector::MATCH_EXACT, '123', null, '' ),
+
+ // Invalid
+ 'Invalid: (empty string)' => array( '' ),
+ 'Invalid: foo' => array( 'foo' ),
+ 'Invalid: [foo' => array( '[foo' ),
+ 'Invalid: [#foo]' => array( '[#foo]' ),
+ 'Invalid: [*|*]' => array( '[*|*]' ),
+ 'Invalid: [ns|*]' => array( '[ns|*]' ),
+ 'Invalid: [* |att]' => array( '[* |att]' ),
+ 'Invalid: [*| att]' => array( '[*| att]' ),
+ 'Invalid: [att * =]' => array( '[att * =]' ),
+ 'Invalid: [att+=val]' => array( '[att+=val]' ),
+ 'Invalid: [att=val ' => array( '[att=val ' ),
+ 'Invalid: [att i]' => array( '[att i]' ),
+ 'Invalid: [att s]' => array( '[att s]' ),
+ "Invalid: [att='val\\n']" => array( "[att='val\n']" ),
+ 'Invalid: [att=val i ' => array( '[att=val i ' ),
+ 'Invalid: [att="val"ix' => array( '[att="val"ix' ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector() {
+ $input = 'el.foo#bar[baz=quux] > .child';
+ $offset = 0;
+ $sel = $this->test_class::test_parse_compound_selector( $input, $offset );
+
+ $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 62653
+ */
+ public function test_parse_empty_selector() {
+ $input = '';
+ $offset = 0;
+ $result = $this->test_class::test_parse_compound_selector( $input, $offset );
+ $this->assertNull( $result );
+ $this->assertSame( 0, $offset );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_list() {
+ $input = 'el1, el2, el.foo#bar[baz=quux]';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_invalid_selector_list() {
+ $input = 'el,,';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_invalid_selector_list2() {
+ $input = 'el!';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_empty_selector_list() {
+ $input = " \t \t\n\r\f";
+ $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
new file mode 100644
index 0000000000000..d94190ff91077
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-select.php
@@ -0,0 +1,87 @@
+' );
+ $this->assertFalse( $processor->select( 'div' ) );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_selectors
+ */
+ public function test_select_all( string $html, string $selector, int $match_count ) {
+ $processor = WP_HTML_Processor::create_full_parser( $html );
+ $count = 0;
+ foreach ( $processor->select_all( $selector ) as $_ ) {
+ $breadcrumb_string = implode( ', ', $processor->get_breadcrumbs() );
+ $this->assertTrue(
+ $processor->get_attribute( 'match' ),
+ "Matched unexpected tag {$processor->get_tag()} @ {$breadcrumb_string}"
+ );
+ ++$count;
+ }
+ $this->assertSame( $match_count, $count, 'Did not match expected number of tags.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_selectors(): array {
+ return array(
+ 'any' => array( '', '*', 5 ),
+ 'quirks mode ID' => array( '
In quirks mode, ID matching is case-insensitive.', '#id', 2 ),
+ 'quirks mode class' => array( '
In quirks mode, class matching is case-insensitive.', '.c', 2 ),
+ 'no-quirks mode ID' => array( '
In no-quirks mode, ID matching is case-sensitive.', '#id', 1 ),
+ 'no-quirks mode class' => array( '
In no-quirks mode, class matching is case-sensitive.', '.c', 1 ),
+ 'any descendant' => array( '', 'section *', 4 ),
+ 'any child 1' => array( '', 'section > *', 2 ),
+ 'any child 2' => array( '
', 'div > *', 1 ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @expectedIncorrectUsage WP_HTML_Processor::select_all
+ *
+ * @dataProvider data_invalid_selectors
+ */
+ public function test_invalid_selector( string $selector ) {
+ $processor = WP_HTML_Processor::create_fragment( 'irrelevant' );
+ $this->assertFalse( $processor->select( $selector ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_invalid_selectors(): array {
+ return array(
+ 'invalid selector' => array( '[invalid!selector]' ),
+
+ // The class selectors below are not allowed in non-final position.
+ 'unsupported child selector' => array( '.parent > .child' ),
+ 'unsupported descendant selector' => array( '.ancestor .descendant' ),
+ );
+ }
+}
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..586e38b4bafb2
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor-select.php
@@ -0,0 +1,111 @@
+' );
+ $this->assertFalse( $processor->select( 'div' ) );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @dataProvider data_selectors
+ */
+ public function test_select( string $html, string $selector, int $match_count ) {
+ $processor = new WP_HTML_Tag_Processor( $html );
+ $count = 0;
+ foreach ( $processor->select_all( $selector ) as $_ ) {
+ $this->assertTrue(
+ $processor->get_attribute( 'match' ),
+ "Matched unexpected tag {$processor->get_tag()}"
+ );
+ ++$count;
+ }
+ $this->assertSame( $match_count, $count, 'Did not match expected number of tags.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_selectors(): array {
+ return array(
+ 'simple type' => array( '
', 'div', 2 ),
+ 'any type' => array( '', '*', 2 ),
+ 'simple class' => array( '', '.x', 2 ),
+ 'simple id' => array( '', '#x', 2 ),
+
+ 'attribute presence' => array( '', '[att]', 2 ),
+ 'attribute empty string match' => array( '', '[att=""]', 2 ),
+ 'attribute value' => array( '', '[att=val]', 2 ),
+ 'attribute quoted value' => array( '
', '[att="::"]', 2 ),
+ 'attribute case insensitive' => array( '
', '[att="VAL"i]', 2 ),
+ 'attribute case sensitive mod' => array( '
', '[att="val"s]', 2 ),
+
+ 'attribute one of' => array( '
', '[att~="b"]', 3 ),
+ 'attribute one of insensitive' => array( '
', '[att~="b"i]', 1 ),
+ 'attribute one of mod sensitive' => array( '
', '[att~="b"s]', 1 ),
+ 'attribute one of whitespace cases' => array( "
", '[att~="b"]', 1 ),
+
+ 'attribute with-hyphen' => array( '
', '[att|="special"]', 2 ),
+ 'attribute with-hyphen insensitive' => array( '
', '[att|="special" i]', 2 ),
+ 'attribute with-hyphen sensitive mod' => array( '
', '[att|="special"s]', 1 ),
+
+ 'attribute prefixed' => array( '
', '[att^="p"]', 2 ),
+ 'attribute prefixed insensitive' => array( '
', '[att^="p"i]', 1 ),
+ 'attribute prefixed sensitive mod' => array( '
', '[att^="p"s]', 1 ),
+
+ 'attribute suffixed' => array( '
', '[att$="x"]', 2 ),
+ 'attribute suffixed insensitive' => array( '
', '[att$="x"i]', 1 ),
+ 'attribute suffixed sensitive mod' => array( '
', '[att$="x"s]', 1 ),
+
+ 'attribute contains' => array( '
', '[att*="x"]', 2 ),
+ 'attribute contains insensitive' => array( '
', '[att*="x"i]', 1 ),
+ 'attribute contains sensitive mod' => array( '
', '[att*="x"s]', 1 ),
+
+ 'list' => array( '
', 'a, p, .class, #id, [att]', 2 ),
+ 'compound' => array( '', 'custom-el[att="bar"][ fruit ~= "banana" i]', 1 ),
+ );
+ }
+
+ /**
+ * @ticket 62653
+ *
+ * @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]' ),
+ );
+ }
+}
diff --git a/tests/phpunit/tests/oembed/getResponseData.php b/tests/phpunit/tests/oembed/getResponseData.php
index 7cd5b1cc66ddf..09a0f3142b319 100644
--- a/tests/phpunit/tests/oembed/getResponseData.php
+++ b/tests/phpunit/tests/oembed/getResponseData.php
@@ -251,6 +251,26 @@ public function test_get_oembed_response_data_with_thumbnail() {
$this->assertLessThanOrEqual( 400, $data['thumbnail_width'] );
}
+ /**
+ * @ticket 62094
+ */
+ public function test_get_oembed_response_data_has_correct_thumbnail_size() {
+ $post = self::factory()->post->create_and_get();
+
+ /* Use a large image as post thumbnail */
+ $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/33772.jpg' );
+ set_post_thumbnail( $post, $attachment_id );
+
+ /* Get the image, sized for 400x??? pixels display */
+ $image = wp_get_attachment_image_src( $attachment_id, array( 400, 0 ) );
+
+ /* Get the oembed data array for a 400 pixels wide embed */
+ $data = get_oembed_response_data( $post, 400 );
+
+ /* Make sure the embed references the small image, not the full-size one. */
+ $this->assertSame( $image[0], $data['thumbnail_url'] );
+ }
+
public function test_get_oembed_response_data_for_attachment() {
$parent = self::factory()->post->create();
$file = DIR_TESTDATA . '/images/canola.jpg';