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