Skip to content
Open
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
9 changes: 5 additions & 4 deletions src/wp-includes/html-api/class-wp-html-open-elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ public function remove_node( WP_HTML_Token $token ): bool {

$position_from_start = $this->count() - $position_from_end - 1;
array_splice( $this->stack, $position_from_start, 1 );
$this->after_element_pop( $item );
$this->after_element_pop( $item, 0 === $position_from_end );
return true;
}

Expand Down Expand Up @@ -731,9 +731,10 @@ public function after_element_push( WP_HTML_Token $item ): void {
*
* @since 6.4.0
*
* @param WP_HTML_Token $item Element that was removed from the stack of open elements.
* @param WP_HTML_Token $item Element that was removed from the stack of open elements.
* @param bool $invoke_pop_handler Whether to call the pop handler.
*/
public function after_element_pop( WP_HTML_Token $item ): void {
public function after_element_pop( WP_HTML_Token $item, bool $invoke_pop_handler = true ): void {
/*
* When adding support for new elements, expand this switch to trap
* cases where the precalculated value needs to change.
Expand Down Expand Up @@ -767,7 +768,7 @@ public function after_element_pop( WP_HTML_Token $item ): void {
break;
}

if ( null !== $this->pop_handler ) {
if ( $invoke_pop_handler && null !== $this->pop_handler ) {
call_user_func( $this->pop_handler, $item );
}
}
Expand Down
103 changes: 102 additions & 1 deletion src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
*/
private $current_element = null;

/**
* Elements removed from the stack of open elements without a normal pop event.
*
* @since 7.1.0
*
* @var array[]
*/
private $non_lifo_breadcrumb_removals = array();

/**
* Context node if created as a fragment parser.
*
Expand Down Expand Up @@ -814,6 +823,10 @@ private function next_visitable_token(): bool {
* tokens works in the meantime and isn't obviously wrong.
*/
if ( empty( $this->element_queue ) ) {
if ( $this->queue_virtual_closer_after_non_lifo_removal() ) {
return $this->next_visitable_token();
}

if ( $this->step() ) {
return $this->next_visitable_token();
}
Expand All @@ -823,6 +836,10 @@ private function next_visitable_token(): bool {
}
}

if ( $this->queue_virtual_closer_after_non_lifo_removal() ) {
return $this->next_visitable_token();
}

// Process the next event on the queue.
$this->current_element = array_shift( $this->element_queue );
if ( ! isset( $this->current_element ) ) {
Expand Down Expand Up @@ -860,6 +877,68 @@ private function next_visitable_token(): bool {
return true;
}

/**
* Queues a virtual closer for a removed node once its subtree closes.
*
* Non-LIFO removals from the stack of open elements do not emit a normal
* pop event because those events blindly pop the current breadcrumb. The
* removed node remains an ancestor of the currently open subtree, but must
* be reported as a virtual closer before visiting the next token after
* that subtree closes.
*
* @since 7.1.0
*
* @return bool Whether a virtual closer was queued.
*/
private function queue_virtual_closer_after_non_lifo_removal(): bool {
if ( empty( $this->non_lifo_breadcrumb_removals ) ) {
return false;
}

$removed_node = end( $this->non_lifo_breadcrumb_removals );
$removed_token = $removed_node['token'];
$breadcrumb_depth = $removed_node['breadcrumb_depth'];

if (
count( $this->breadcrumbs ) !== $breadcrumb_depth ||
empty( $this->breadcrumbs ) ||
end( $this->breadcrumbs ) !== $removed_token->node_name
) {
return false;
}

// At EOF, normal stack pops may be queued and processed after the stack is empty.
$adjusted_current_node = $this->get_adjusted_current_node();

if ( isset( $adjusted_current_node ) && end( $this->breadcrumbs ) === $adjusted_current_node->node_name ) {
return false;
}

/*
* The depth and node-name checks above cannot distinguish the removed
* element from a same-named element at the same depth; identity is
* recovered here. If a queued POP closes a different element with the
* same name, that element owns the current breadcrumb and the virtual
* closer must wait for it.
*/
$next_event = reset( $this->element_queue );
if (
false !== $next_event &&
WP_HTML_Stack_Event::POP === $next_event->operation &&
$next_event->token !== $removed_token &&
$next_event->token->node_name === $removed_token->node_name
) {
return false;
}

array_pop( $this->non_lifo_breadcrumb_removals );
array_unshift(
$this->element_queue,
new WP_HTML_Stack_Event( $removed_token, WP_HTML_Stack_Event::POP, 'virtual' )
);
return true;
}

/**
* Indicates if the current tag token is a tag closer.
*
Expand Down Expand Up @@ -2848,7 +2927,28 @@ private function step_in_body(): bool {
case 'A':
$this->run_adoption_agency_algorithm();
$this->state->active_formatting_elements->remove_node( $item );
$this->state->stack_of_open_elements->remove_node( $item );
$is_current_node = $item === $this->state->stack_of_open_elements->current_node();

/*
* The removed node's breadcrumb sits at its position in the
* stack of open elements: one crumb for each open element at
* or below it. Fragment parsers carry an extra crumb for the
* context node, which never appears on the stack.
*/
$stack_position = 0;
foreach ( $this->state->stack_of_open_elements->walk_down() as $node ) {
++$stack_position;
if ( $node === $item ) {
break;
}
}

if ( $this->state->stack_of_open_elements->remove_node( $item ) && ! $is_current_node ) {
$this->non_lifo_breadcrumb_removals[] = array(
'token' => $item,
'breadcrumb_depth' => isset( $this->context_node ) ? $stack_position + 1 : $stack_position,
);
}
break 2;
}
}
Expand Down Expand Up @@ -5675,6 +5775,7 @@ public function seek( $bookmark_name ): bool {
$this->state->current_token = null;
$this->current_element = null;
$this->element_queue = array();
$this->non_lifo_breadcrumb_removals = array();

/*
* The absence of a context node indicates a full parse.
Expand Down
Loading
Loading