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' ); + } +}