diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 0dfa57f30f2aab..9539f0d626e9d6 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -1412,6 +1412,49 @@ public function get_attribute( $name ) { return html_entity_decode( $raw_value ); } + /** + * Returns the lowercase names of all attributes matching a given prefix in the currently-opened tag. + * + * Note that matching is case-insensitive. This is in accordance with the spec: + * + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
Test
' ); + * $p->next_tag( [ 'class_name' => 'test' ] ) === true; + * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); + * + * $p->next_tag( [] ) === false; + * $p->get_attribute_names_with_prefix( 'data-' ) === null; + *
+ * + * @since 6.2.0 + * + * @param string $prefix Prefix of requested attribute names. + * @return array|null List of attribute names, or `null` if not at a tag. + */ + function get_attribute_names_with_prefix( $prefix ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $prefix ); + + $matches = array(); + foreach ( array_keys( $this->attributes ) as $attr_name ) { + if ( str_starts_with( $attr_name, $comparable ) ) { + $matches[] = $attr_name; + } + } + return $matches; + } + /** * Returns the lowercase name of the currently-opened tag. * diff --git a/phpunit/html/wp-html-tag-processor-test.php b/phpunit/html/wp-html-tag-processor-test.php index e6b3d4d8071af5..6750410f26d43c 100644 --- a/phpunit/html/wp-html-tag-processor-test.php +++ b/phpunit/html/wp-html-tag-processor-test.php @@ -74,6 +74,19 @@ public function test_get_attribute_returns_null_when_not_in_open_tag() { $this->assertNull( $p->get_attribute( 'class' ), 'Accessing an attribute of a non-existing tag did not return null' ); } + /** + * @ticket 56299 + * + * @covers next_tag + * @covers get_attribute + */ + public function test_get_attribute_returns_null_when_in_closing_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $this->assertTrue( $p->next_tag( 'div' ), 'Querying an existing tag did not return true' ); + $this->assertTrue( $p->next_tag( array( 'tag_closers' => 'visit' ) ), 'Querying an existing closing tag did not return true' ); + $this->assertNull( $p->get_attribute( 'class' ), 'Accessing an attribute of a closing tag did not return null' ); + } + /** * @ticket 56299 * @@ -195,6 +208,87 @@ public function test_set_attribute_is_case_insensitive() { $this->assertEquals( '
Test
', $p->get_updated_html(), 'A case-insensitive set_attribute call did not update the existing attribute.' ); } + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_before_finding_tags() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ) ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_when_not_in_open_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'p' ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing attributes of a non-existing tag did not return null' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_when_in_closing_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'div' ); + $p->next_tag( array( 'tag_closers' => 'visit' ) ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing attributes of a closing tag did not return null' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_empty_array_when_no_attributes_present() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'div' ); + $this->assertSame( array(), $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing the attributes on a tag without any did not return an empty array' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_matching_attribute_names_in_lowercase() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag(); + $this->assertSame( + array( 'data-enabled', 'data-test-id' ), + $p->get_attribute_names_with_prefix( 'data-' ) + ); + } + + /** + * @ticket 56299 + * + * @covers set_attribute + * @covers get_updated_html + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_attribute_added_by_set_attribute() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag(); + $p->set_attribute( 'data-test-id', '14' ); + $this->assertSame( + '
Test
', + $p->get_updated_html(), + "Updated HTML doesn't include attribute added via set_attribute" + ); + $this->assertSame( + array( 'data-test-id', 'data-foo' ), + $p->get_attribute_names_with_prefix( 'data-' ), + "Accessing attribute names doesn't find attribute added via set_attribute" + ); + } + /** * @ticket 56299 *