Skip to content

Commit b19c465

Browse files
committed
HTML API: Derive removed anchor breadcrumb depth from stack position.
Scanning breadcrumbs by node name latched onto same-named foreign elements (MathML or SVG A) between the removed HTML anchor and the integration point, storing the wrong depth. The virtual closer then never fired and the stale anchor breadcrumb persisted for the rest of the document. Record the removed node's position in the stack of open elements instead, accounting for the fragment parser's context crumb.
1 parent 779e594 commit b19c465

2 files changed

Lines changed: 60 additions & 5 deletions

File tree

src/wp-includes/html-api/class-wp-html-processor.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2921,15 +2921,25 @@ private function step_in_body(): bool {
29212921
$this->run_adoption_agency_algorithm();
29222922
$this->state->active_formatting_elements->remove_node( $item );
29232923
$is_current_node = $item === $this->state->stack_of_open_elements->current_node();
2924-
if ( $this->state->stack_of_open_elements->remove_node( $item ) && ! $is_current_node ) {
2925-
$breadcrumb_depth = count( $this->breadcrumbs );
2926-
while ( 0 < $breadcrumb_depth && $this->breadcrumbs[ $breadcrumb_depth - 1 ] !== $item->node_name ) {
2927-
--$breadcrumb_depth;
2924+
2925+
/*
2926+
* The removed node's breadcrumb sits at its position in the
2927+
* stack of open elements: one crumb for each open element at
2928+
* or below it. Fragment parsers carry an extra crumb for the
2929+
* context node, which never appears on the stack.
2930+
*/
2931+
$stack_position = 0;
2932+
foreach ( $this->state->stack_of_open_elements->walk_down() as $node ) {
2933+
++$stack_position;
2934+
if ( $node === $item ) {
2935+
break;
29282936
}
2937+
}
29292938

2939+
if ( $this->state->stack_of_open_elements->remove_node( $item ) && ! $is_current_node ) {
29302940
$this->non_lifo_breadcrumb_removals[] = array(
29312941
'token' => $item,
2932-
'breadcrumb_depth' => $breadcrumb_depth,
2942+
'breadcrumb_depth' => isset( $this->context_node ) ? $stack_position + 1 : $stack_position,
29332943
);
29342944
}
29352945
break 2;

tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,51 @@ public function test_removes_outer_anchor_breadcrumb_after_mathml_text_integrati
492492
);
493493
}
494494

495+
/**
496+
* Ensures that a removed outer A element's breadcrumb is not confused with
497+
* a same-named foreign element between it and the integration point.
498+
*
499+
* Foreign A elements never participate in the active formatting elements,
500+
* so the removed node is the outer HTML A element, not the foreign one.
501+
*
502+
* @ticket 61576
503+
*
504+
* @covers WP_HTML_Processor::get_breadcrumbs
505+
* @covers WP_HTML_Processor::matches_breadcrumbs
506+
*
507+
* @dataProvider data_intervening_foreign_anchor_html
508+
*
509+
* @param string $html HTML with a foreign A element between the removed outer A element and the integration point.
510+
*/
511+
public function test_removes_outer_anchor_breadcrumb_with_intervening_foreign_anchor( string $html ) {
512+
$processor = WP_HTML_Processor::create_fragment( $html );
513+
514+
$this->assertTrue( $processor->next_tag( 'SPAN' ), 'Failed to find the SPAN element after the foreign subtree.' );
515+
516+
$this->assertSame(
517+
array( 'HTML', 'BODY', 'SPAN' ),
518+
$processor->get_breadcrumbs(),
519+
'The SPAN element after the foreign subtree should not remain nested inside the removed outer A element.'
520+
);
521+
522+
$this->assertFalse(
523+
$processor->matches_breadcrumbs( array( 'A', 'SPAN' ) ),
524+
'The SPAN element should not match breadcrumbs inside the removed outer A element.'
525+
);
526+
}
527+
528+
/**
529+
* Data provider.
530+
*
531+
* @return array[]
532+
*/
533+
public static function data_intervening_foreign_anchor_html() {
534+
return array(
535+
'MathML A before text integration point' => array( '<a><math><a><mtext>x<a>y</a></mtext></a></math>z<span>t' ),
536+
'SVG A before integration point' => array( '<a><svg><a><foreignObject>x<a>y</a></foreignObject></a></svg>z<span>t' ),
537+
);
538+
}
539+
495540
/**
496541
* Ensures that an outer A element removed from the stack of open elements
497542
* remains visitable as a virtual closer after its existing child subtree closes.

0 commit comments

Comments
 (0)