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 975f21a0f0d77..f8653022454b6 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -609,25 +609,30 @@ public function next_token(): bool { * until there are events or until there are no more * tokens works in the meantime and isn't obviously wrong. */ - while ( empty( $this->element_queue ) && $this->step() ) { - continue; + if ( empty( $this->element_queue ) && $this->step() ) { + return $this->next_token(); } // Process the next event on the queue. $this->current_element = array_shift( $this->element_queue ); if ( ! isset( $this->current_element ) ) { - return false; + // There are no tokens left, so close all remaining open elements. + while ( $this->state->stack_of_open_elements->pop() ) { + continue; + } + + return empty( $this->element_queue ) ? false : $this->next_token(); } $is_pop = WP_HTML_Stack_Event::POP === $this->current_element->operation; /* * The root node only exists in the fragment parser, and closing it - * indicates that the parse is complete. Stop before popping if from + * indicates that the parse is complete. Stop before popping it from * the breadcrumbs. */ if ( 'root-node' === $this->current_element->token->bookmark_name ) { - return ! $is_pop && $this->next_token(); + return $this->next_token(); } // Adjust the breadcrumbs for this event. @@ -638,7 +643,7 @@ public function next_token(): bool { } // Avoid sending close events for elements which don't expect a closing. - if ( $is_pop && ! static::expects_closer( $this->current_element->token ) ) { + if ( $is_pop && ! $this->expects_closer( $this->current_element->token ) ) { return $this->next_token(); } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 12f36ca742989..01e0f4f02c0b5 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -475,6 +475,47 @@ public static function data_html_with_target_element_and_depth_of_next_node_in_b ); } + /** + * Ensures that elements which are unopened at the end of a document are implicitly closed. + * + * @ticket 61576 + */ + public function test_closes_unclosed_elements() { + $processor = WP_HTML_Processor::create_fragment( '
' ); + + $this->assertTrue( + $processor->next_tag( 'SPAN' ), + 'Could not find SPAN element: check test setup.' + ); + + // This is the end of the document, but there should be three closing events. + $processor->next_token(); + $this->assertSame( + 'SPAN', + $processor->get_tag(), + 'Should have found implicit SPAN closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'P', + $processor->get_tag(), + 'Should have found implicit P closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'DIV', + $processor->get_tag(), + 'Should have found implicit DIV closing tag.' + ); + + $this->assertFalse( + $processor->next_token(), + "Should have failed to find any more tokens but found a '{$processor->get_token_name()}'" + ); + } + /** * Ensures that subclasses can be created from ::create_fragment method. *