diff --git a/src/wp-includes/html-api/class-wp-html-active-formatting-elements.php b/src/wp-includes/html-api/class-wp-html-active-formatting-elements.php index 9f7fee9076243..0d5772652a032 100644 --- a/src/wp-includes/html-api/class-wp-html-active-formatting-elements.php +++ b/src/wp-includes/html-api/class-wp-html-active-formatting-elements.php @@ -43,6 +43,22 @@ class WP_HTML_Active_Formatting_Elements { */ private $stack = array(); + /** + * Returns the node at the given index in the list of active formatting elements. + * + * Do not use this method; it is meant to be used only by the HTML Processor. + * + * @since 6.7.0 + * + * @access private + * + * @param int $index Number of nodes from the top node to return. + * @return WP_HTML_Token|null Node at the given index in the stack, if one exists, otherwise null. + */ + public function at( $index ) { + return $this->stack[ $index ]; + } + /** * Reports if a specific node is in the stack of active formatting elements. * @@ -86,6 +102,16 @@ public function current_node() { return $current_node ? $current_node : null; } + /** + * Inserts a marker at the end of the list of active formatting elements. + * + * @since 6.7.0 + */ + public function insert_marker() { + $marker = new WP_HTML_Token( null, 'marker', false ); + $this->push( $marker ); + } + /** * Pushes a node onto the stack of active formatting 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 588d2fbe7d7c9..bcdcdf93573f5 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -2832,15 +2832,19 @@ private function generate_implied_end_tags_thoroughly() { * @return bool Whether any formatting elements needed to be reconstructed. */ private function reconstruct_active_formatting_elements() { + $count = $this->state->active_formatting_elements->count(); + /* * > If there are no entries in the list of active formatting elements, then there is nothing * > to reconstruct; stop this algorithm. */ - if ( 0 === $this->state->active_formatting_elements->count() ) { + if ( 0 === $count ) { return false; } - $last_entry = $this->state->active_formatting_elements->current_node(); + // Start at the last node in the list of active formatting elements. + $currently_at = $count - 1; + $last_entry = $this->state->active_formatting_elements->at( $currently_at ); if ( /* @@ -2859,8 +2863,39 @@ private function reconstruct_active_formatting_elements() { return false; } - $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' ); + $entry = $last_entry; + + while ( $currently_at >= 0 ) { + if ( 0 === $currently_at ) { + goto create; + } + $entry = $this->state->active_formatting_elements->at( --$currently_at ); + + /* + * > If entry is neither a marker nor an element that is also in the stack of open elements, + * > go to the step labeled rewind. + */ + if ( 'marker' === $entry->node_name || $this->state->stack_of_open_elements->contains_node( $entry ) ) { + break; + } + } + + advance: + $entry = $this->state->active_formatting_elements->at( ++$currently_at ); + + create: + $this->insert_html_element( $entry ); + + /* + * > Replace the entry for entry in the list with an entry for new element. + * This doesn't need to happen here since no DOM is being created. + */ + + if ( $count - 1 !== $currently_at ) { + goto advance; + } + + return true; } /** diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index b842703a7a135..8294aa5b198b3 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -112,18 +112,23 @@ public function test_clear_to_navigate_after_seeking() { } /** - * Ensures that support is added for reconstructing active formatting elements - * before the HTML Processor handles situations with unclosed formats requiring it. + * Ensures that support is added for reconstructing active formatting elements. * * @ticket 58517 * * @covers WP_HTML_Processor::reconstruct_active_formatting_elements */ - public function test_fails_to_reconstruct_formatting_elements() { - $processor = WP_HTML_Processor::create_fragment( '

One

Two

Three

Four' ); + public function test_reconstructs_formatting_elements() { + $processor = WP_HTML_Processor::create_fragment( '

One

Two

Three

Four' ); $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find first EM.' ); - $this->assertFalse( $processor->next_tag( 'EM' ), 'Should have aborted before finding second EM as it required reconstructing the first EM.' ); + $this->assertSame( array( 'HTML', 'BODY', 'P', 'EM' ), $processor->get_breadcrumbs(), 'Found incorrect breadcrumbs for first EM.' ); + $this->assertTrue( $processor->next_tag( 'SPAN' ), 'Could not find test span.' ); + $this->assertSame( + array( 'HTML', 'BODY', 'P', 'EM', 'EM', 'SPAN' ), + $processor->get_breadcrumbs(), + 'Found incorrect breadcrumbs for test SPAN; should have created two EMs.' + ); } /** diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 403f40a1da032..5ec846df16fec 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -219,45 +219,55 @@ public static function data_unsupported_elements() { } /** - * @ticket 58517 - * - * @dataProvider data_unsupported_markup + * Ensures that formats inside unclosed A elements are reconstructed. * - * @param string $html HTML containing unsupported markup. + * @ticket 61576 */ - public function test_fails_when_encountering_unsupported_markup( $html, $description ) { - $processor = WP_HTML_Processor::create_fragment( $html ); - - while ( $processor->next_token() && null === $processor->get_attribute( 'supported' ) ) { - continue; - } + public function test_reconstructs_formatting_from_unclosed_a_elements() { + $processor = WP_HTML_Processor::create_fragment( 'Click Here' ); - $this->assertNull( - $processor->get_last_error(), - 'Bailed on unsupported input before finding supported checkpoint: check test code.' + $processor->next_tag( 'STRONG' ); + $this->assertSame( + array( 'HTML', 'BODY', 'A', 'STRONG' ), + $processor->get_breadcrumbs(), + 'Failed to construct starting breadcrumbs properly.' ); - $this->assertTrue( $processor->get_attribute( 'supported' ), 'Did not find required supported element.' ); - $processor->next_token(); - $this->assertNotNull( $processor->get_last_error(), "Didn't properly reject unsupported markup: {$description}" ); + $processor->next_tag( 'BIG' ); + $this->assertSame( + array( 'HTML', 'BODY', 'STRONG', 'A', 'BIG' ), + $processor->get_breadcrumbs(), + 'Failed to reconstruct the active formatting elements after an unclosed A element.' + ); } /** - * Data provider. + * Ensures that unclosed A elements are reconstructed. * - * @return array[] + * @ticket 61576 */ - public static function data_unsupported_markup() { - return array( - 'A with formatting following unclosed A' => array( - 'Click Here', - 'Unclosed formatting requires complicated reconstruction.', - ), + public function test_reconstructs_unclosed_a_elements() { + $processor = WP_HTML_Processor::create_fragment( '

' ); - 'A after unclosed A inside DIV' => array( - '
', - 'A is a formatting element, which requires more complicated reconstruction.', - ), + $processor->next_tag( 'DIV' ); + $this->assertSame( + array( 'HTML', 'BODY', 'DIV' ), + $processor->get_breadcrumbs(), + 'Failed to construct breadcrumbs properly - the DIV should have closed the A element.' + ); + + // When the DIV re-opens, it reconstructs an unclosed A, then the A in the text is a second A. + $processor->next_tag( 'A' ); + $this->assertSame( + array( 'HTML', 'BODY', 'DIV', 'A' ), + 'Failed to create proper breadcrumbs for recreated A element.' + ); + + // This is the one that's second in the raw text. + $processor->next_tag( 'A' ); + $this->assertSame( + array( 'HTML', 'BODY', 'DIV', 'A' ), + 'Failed to create proper breadcrumbs for explicit A element - this A should have closed the reconstructed A.' ); }