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