diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 8fb53e272cdf8..1af1c7c8f752c 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -106,7 +106,7 @@ public function current_node() { * * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope * - * @param string $tag_name Name of tag check, or the class constant HEADING_ELEMENTS to specify H1-H6. + * @param string $tag_name Name of tag check, or WP_HTML_Tag_Processor::H1_H6_ELEMENTS for any H1 - H6. * @param string[] $termination_list List of elements that terminate the search. * @return bool Whether the element was found in a specific scope. */ @@ -117,7 +117,7 @@ public function has_element_in_specific_scope( $tag_name, $termination_list ) { } if ( - self::HEADING_ELEMENTS === $tag_name && + WP_HTML_Tag_Processor::H1_H6_ELEMENTS === $tag_name && in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; @@ -271,7 +271,7 @@ public function pop() { * @see WP_HTML_Open_Elements::pop * * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements, - * or the class constant HEADING_ELEMENTS to specify any of H1-H6. + * or WP_HTML_Tag_Processor::H1_H6_ELEMENTS to specify any of H1 - H6. * @return bool Whether a tag of the given name was found and popped off of the stack of open elements. */ public function pop_until( $tag_name ) { @@ -279,7 +279,7 @@ public function pop_until( $tag_name ) { $this->pop(); if ( - self::HEADING_ELEMENTS === $tag_name && + WP_HTML_Tag_Processor::H1_H6_ELEMENTS === $tag_name && in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; @@ -444,15 +444,4 @@ public function after_element_pop( $item ) { break; } } - - /** - * Represents the collection of H1-H6 elements. - * - * @since 6.5.0 - * - * @see has_element_in_scope() - * - * @var string - */ - const HEADING_ELEMENTS = 'heading-elements'; } 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 228d0983c64af..865db010b6892 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -694,11 +694,7 @@ private function step_in_body() { case '-H4': case '-H5': case '-H6': - if ( - ! $this->state->stack_of_open_elements->has_element_in_scope( - WP_HTML_Open_Elements::HEADING_ELEMENTS - ) - ) { + if ( ! $this->state->stack_of_open_elements->has_element_in_scope( WP_HTML_Tag_Processor::H1_H6_ELEMENTS ) ) { /* * This is a parse error; ignore the token. * @@ -713,7 +709,7 @@ private function step_in_body() { // @TODO: Record parse error: this error doesn't impact parsing. } - $this->state->stack_of_open_elements->pop_until( WP_HTML_Open_Elements::HEADING_ELEMENTS ); + $this->state->stack_of_open_elements->pop_until( WP_HTML_Tag_Processor::H1_H6_ELEMENTS ); 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 19cca778ea6ee..5bd7ad1aad083 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 @@ -2401,6 +2401,7 @@ private function parse_query( $query ) { * Checks whether a given tag and its attributes match the search criteria. * * @since 6.2.0 + * @since 6.5.0 Allows matching any of the H1 - H6 tag names with the WP_HTML_Tag_Processor::H1_H6_ELEMENTS constant. * * @return bool Whether the given tag and its attribute match the search criteria. */ @@ -2409,8 +2410,31 @@ private function matches() { return false; } + // Does the tag name match the requested tag name in a case-insensitive manner? - if ( null !== $this->sought_tag_name ) { + if ( self::H1_H6_ELEMENTS === $this->sought_tag_name ) { + /* + * H1 through H6 are special because they act like one tag + * name but are distinct. It's common enough to want to stop + * at any of them without knowing in advance which one to + * look for; the class constant aids this by representing + * the entire set of six elements: H1, H2, H3, H4, H5, H6. + */ + + if ( 2 !== $this->tag_name_length ) { + return false; + } + + $c = $this->html[ $this->tag_name_starts_at ]; + if ( 'h' !== $c && 'H' !== $c ) { + return false; + } + + $c = $this->html[ $this->tag_name_starts_at + 1 ]; + if ( '1' !== $c && '2' !== $c && '3' !== $c && '4' !== $c && '5' !== $c && '6' !== $c ) { + return false; + } + } elseif ( null !== $this->sought_tag_name ) { /* * String (byte) length lookup is fast. If they aren't the * same length then they can't be the same string values. @@ -2447,4 +2471,17 @@ private function matches() { return true; } + + // Class constants that would otherwise be distracting if found at the top of the document. + + /** + * Represents the collection of H1-H6 elements. + * + * @since 6.5.0 + * + * @see has_element_in_scope() + * + * @var string + */ + const H1_H6_ELEMENTS = 'Match on any of H1, H2, H3, H4, H5, H6'; } diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index 4469f90c4f276..58d380d06b506 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -509,6 +509,77 @@ public function test_next_tag_matches_decoded_class_names() { $this->assertTrue( $p->next_tag( array( 'class_name' => '' ) ), 'Failed to find tag with HTML-encoded class name.' ); } + /** + * Ensures that the H1_H6_ELEMENTS constant leads to matches for H1 through H6 elements. + * + * @ticket {TICKET_NUMBER} + * + * @covers WP_HTML_Tag_Processor::next_tag + * + * @dataProvider data_h_tag_names + * + * @param string $h_tag_name One of H1 through H6, case-insensitive. + */ + public function test_next_tag_matches_h1_through_h6_with_the_class_constant( $h_tag_name ) { + $p = new WP_HTML_Tag_Processor( "
<{$h_tag_name}>
" ); + + $this->assertTrue( $p->next_tag( WP_HTML_Tag_Processor::H1_H6_ELEMENTS ), "Failed to find {$h_tag_name} tag opener." ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_h_tag_names() { + return array( + 'H1' => array( 'H1' ), + 'H2' => array( 'H2' ), + 'H3' => array( 'H3' ), + 'H4' => array( 'H4' ), + 'H5' => array( 'H5' ), + 'H6' => array( 'H6' ), + ); + } + + /** + * Ensures that the H1_H6_ELEMENTS constant doesn't lead to matches on + * tag names that look similar to H1 - H6 but aren't those elements. + * + * @ticket {TICKET_NUMBER} + * + * @covers WP_HTML_Tag_Processor::next_tag + * + * @dataProvider data_invalid_h_tag_names + * + * @param string $invalid_h_tag_name Tag names that look like H1 through H6 but are not those tag names. + */ + public function test_next_tag_does_not_match_invalid_h_elements_with_the_class_constant( $invalid_h_tag_name ) { + $p = new WP_HTML_Tag_Processor( "
<{$invalid_h_tag_name}>
" ); + + $this->assertFalse( $p->next_tag( WP_HTML_Tag_Processor::H1_H6_ELEMENTS ), "Found {$p->get_tag()} when looking for {$invalid_h_tag_name} element and should have found nothing." ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public function data_invalid_h_tag_names() { + return array( + 'H0' => array( 'H0' ), + 'H7' => array( 'H7' ), + 'H13' => array( 'H13' ), + + /* + * Preserve the FULLWIDTH DIGIT SIX key because PHPUnit interprets '6' as + * a numeric array item and reports "data set 0" instead of "6". + */ + '6' => array( '6' ), + 'H4-CUSTOM' => array( 'H4-CUSTOM' ), + ); + } + /** * @ticket 56299 * @ticket 57852