Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,11 @@ public function next_tag( $query = null ) {
public function next_token() {
$found_a_token = parent::next_token();

if ( '#tag' === $this->get_token_type() ) {
$this->step( self::PROCESS_CURRENT_NODE );
switch ( $this->get_token_type() ) {
case '#tag':
case '#text':
$this->step( self::PROCESS_CURRENT_NODE );
break;
}

return $found_a_token;
Expand Down Expand Up @@ -536,6 +539,11 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) {

if ( self::PROCESS_NEXT_NODE === $node_to_process ) {
while ( parent::next_token() && '#tag' !== $this->get_token_type() ) {
if ( '#text' === $this->get_token_type() && $this->has_active_formats_needing_reconstruction() ) {
$this->last_error = self::ERROR_UNSUPPORTED;
return false;
}

continue;
}
}
Expand Down Expand Up @@ -1498,6 +1506,44 @@ private function reconstruct_active_formatting_elements() {
throw new WP_HTML_Unsupported_Exception( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
}

/**
* Indicates if there are active formatting elements needing reconstruction.
*
* @since 6.5.0
*
* @return bool False if reconstruction would definitely not create any new elements.
*/
private function has_active_formats_needing_reconstruction() {
/*
* > 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() ) {
return false;
}

$last_entry = $this->state->active_formatting_elements->current_node();
if (

/*
* > If the last (most recently added) entry in the list of active formatting elements is a marker;
* > stop this algorithm.
*/
'marker' === $last_entry->node_name ||

/*
* > If the last (most recently added) entry in the list of active formatting elements is an
* > element that is in the stack of open elements, then there is nothing to reconstruct;
* > stop this algorithm.
*/
$this->state->stack_of_open_elements->contains_node( $last_entry )
) {
return false;
}

return true;
}

/**
* Runs the adoption agency algorithm.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
/**
* Unit tests for the HTML API ensuring proper handling of behaviors related to
* active format reconstruction.
*
* @package WordPress
* @subpackage HTML-API
*
* @since 6.5.0
*
* @group html-api
*
* @coversDefaultClass WP_HTML_Processor
*/
class Tests_HtmlApi_WpHtmlSupportRequiredActiveFormatReconstruction extends WP_UnitTestCase {
/**
* Ensures that active formats are properly reconstructed when visiting text nodes,
* verifying that the proper breadcrumbs are maintained when scanning through HTML.
*
* @ticket 60455
*/
public function test_reconstructs_active_formats_on_text_nodes() {
$processor = WP_HTML_Processor::create_fragment( '<p><b>One<p><source>Two<source>' );

// The SOURCE element doesn't trigger reconstruction, and this test asserts that.
$this->assertTrue(
$processor->next_tag( 'SOURCE' ),
'Should have found the first custom element.'
);

$this->assertSame(
array( 'HTML', 'BODY', 'P', 'SOURCE' ),
$processor->get_breadcrumbs(),
'Should have closed formatting element at first P element.'
);

/*
* There are two ways this test could fail. One is to appropriately find the
* second text node but fail to reconstruct the implicitly-closed B element.
* The other way is to fail to abort when encountering the second text node
* because the kind of active format reconstruction isn't supported.
*
* At the time of writing this test, the HTML Processor bails whenever it
* needs to reconstruct active formats, unless there are no active formats.
* To ensure that this test properly works once that support is expanded,
* it's written to verify both circumstances. Once support is added, this
* can be simplified to only contain the first clause of the conditional.
*
* The use of the SOURCE element is important here because most elements
* will also trigger reconstruction, which would conflate the test results
* with the text node triggering reconstruction. The SOURCE element won't
* do this, making it neutral. Therefore, the implicitly-closed B element
* will only be reconstructed by the text node.
*/

if ( $processor->next_tag( 'SOURCE' ) ) {
echo "\e[32mSOURCE\e[m\n";
$this->assertSame(
array( 'HTML', 'BODY', 'P', 'B', 'SOURCE' ),
$processor->get_breadcrumbs(),
'Should have reconstructed the implicitly-closed B element.'
);
} else {
$this->assertSame(
WP_HTML_Processor::ERROR_UNSUPPORTED,
$processor->get_last_error(),
'Should have aborted for incomplete active format reconstruction when encountering the second text node.'
);
}
}
}