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 68bd3418f2..c8e898389c 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -626,6 +626,37 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
$this->insert_html_element( $this->current_token );
return true;
+ /*
+ * > Any other start tag
+ */
+ case '+SPAN':
+ $this->reconstruct_active_formatting_elements();
+ $this->insert_html_element( $this->current_token );
+ return true;
+
+ /*
+ * Any other end tag
+ */
+ case '-SPAN':
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
+ // > If node is an HTML element with the same tag name as the token, then:
+ if ( $item->node_name === $tag_name ) {
+ $this->generate_implied_end_tags( $tag_name );
+
+ // > If node is not the current node, then this is a parse error.
+
+ $this->state->stack_of_open_elements->pop_until( $tag_name );
+ return true;
+ }
+
+ // > Otherwise, if node is in the special category, then this is a parse error; ignore the token, and return.
+ if ( self::is_special( $item->node_name ) ) {
+ return $this->step();
+ }
+ }
+ // Execution should not reach here; if it does then something went wrong.
+ return false;
+
default:
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." );
@@ -873,7 +904,7 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
*
* @since 6.4.0
*
- * @throws Exception
+ * @throws WP_HTML_Unsupported_Exception
*
* @see https://html.spec.whatwg.org/#generate-implied-end-tags
*
@@ -893,6 +924,26 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
}
}
+ /*
+ * Closes elements that have implied end tags, thoroughly.
+ *
+ * See the HTML specification for an explanation why this is
+ * different from {@see WP_HTML_Processor::generate_implied_end_tags}.
+ *
+ * @since 6.4.0
+ *
+ * @see https://html.spec.whatwg.org/#generate-implied-end-tags
+ */
+ private function generate_implied_end_tags_thoroughly() {
+ $elements_with_implied_end_tags = array(
+ 'P',
+ );
+
+ while ( in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) {
+ $this->state->stack_of_open_elements->pop();
+ }
+ }
+
/**
* Reconstructs the active formatting elements.
*
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
index 4bf46a173f..229ae38c08 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
@@ -49,6 +49,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
'IMG',
'P',
'SMALL',
+ 'SPAN',
'STRIKE',
'STRONG',
'TT',
@@ -191,7 +192,6 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
'SLOT',
'SOURCE',
'SPACER', // Deprecated
- 'SPAN',
'STYLE',
'SUB',
'SUMMARY',
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
new file mode 100644
index 0000000000..e16d496cb4
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
@@ -0,0 +1,68 @@
+
' );
+
+ $p->next_tag( 'P' );
+ $this->assertSame( 'P', $p->get_tag(), "Expected to start test on P element but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'P' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past P tag to expected DIV opener.' );
+ $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should still be open and DIV should be its child.' );
+ }
+
+ /*
+ * Verifies that when "in body" and encountering "any other end tag"
+ * that the HTML processor closes appropriate elements on the stack of
+ * open elements up to the matching opening.
+ *
+ * @ticket 58907
+ *
+ * @since 6.4.0
+ *
+ * @covers WP_HTML_Processor::step_in_body
+ */
+ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element() {
+ $p = WP_HTML_Processor::createFragment( '
' );
+
+ $p->next_tag( 'CODE' );
+ $this->assertSame( 'CODE', $p->get_tag(), "Expected to start test on CODE element but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' );
+ $this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past SPAN closer to expected DIV opener.' );
+ $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should be closed and DIV should be its sibling.' );
+ }
+}
diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php
new file mode 100644
index 0000000000..b9dbbc8cb8
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php
@@ -0,0 +1,101 @@
+" );
+
+ $this->assertFalse( $p->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." );
+ }
+
+ /**
+ * Generating implied end tags walks up the stack of open elements
+ * as long as any of the following missing elements is the current node.
+ *
+ * @since 6.4.0
+ *
+ * @ticket 58907
+ *
+ * @covers WP_HTML_Processor::generate_implied_end_tags
+ */
+ public function test_generate_implied_end_tags_needs_support() {
+ $this->ensure_support_is_added_everywhere( 'DD' );
+ $this->ensure_support_is_added_everywhere( 'DT' );
+ $this->ensure_support_is_added_everywhere( 'LI' );
+ $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
+ $this->ensure_support_is_added_everywhere( 'OPTION' );
+ $this->ensure_support_is_added_everywhere( 'RB' );
+ $this->ensure_support_is_added_everywhere( 'RP' );
+ $this->ensure_support_is_added_everywhere( 'RT' );
+ $this->ensure_support_is_added_everywhere( 'RTC' );
+ }
+
+ /**
+ * Generating implied end tags thoroughly walks up the stack of open elements
+ * as long as any of the following missing elements is the current node.
+ *
+ * @since 6.4.0
+ *
+ * @ticket 58907
+ *
+ * @covers WP_HTML_Processor::generate_implied_end_tags_thoroughly
+ */
+ public function test_generate_implied_end_tags_thoroughly_needs_support() {
+ $this->ensure_support_is_added_everywhere( 'CAPTION' );
+ $this->ensure_support_is_added_everywhere( 'COLGROUP' );
+ $this->ensure_support_is_added_everywhere( 'DD' );
+ $this->ensure_support_is_added_everywhere( 'DT' );
+ $this->ensure_support_is_added_everywhere( 'LI' );
+ $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
+ $this->ensure_support_is_added_everywhere( 'OPTION' );
+ $this->ensure_support_is_added_everywhere( 'RB' );
+ $this->ensure_support_is_added_everywhere( 'RP' );
+ $this->ensure_support_is_added_everywhere( 'RT' );
+ $this->ensure_support_is_added_everywhere( 'RTC' );
+ $this->ensure_support_is_added_everywhere( 'TBODY' );
+ $this->ensure_support_is_added_everywhere( 'TD' );
+ $this->ensure_support_is_added_everywhere( 'TFOOT' );
+ $this->ensure_support_is_added_everywhere( 'TH' );
+ $this->ensure_support_is_added_everywhere( 'HEAD' );
+ $this->ensure_support_is_added_everywhere( 'TR' );
+ }
+}