From 57f2b7533e4815d861548c691299be0ed6a7e13f Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Thu, 18 Jun 2026 20:44:06 +0200
Subject: [PATCH 1/6] Implement adoption agency "any other end tag" handling
---
.../html-api/class-wp-html-processor.php | 92 ++++++++++++-------
1 file changed, 59 insertions(+), 33 deletions(-)
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 967d616129647..2db46e04119b5 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -2853,7 +2853,10 @@ private function step_in_body(): bool {
break 2;
case 'A':
- $this->run_adoption_agency_algorithm();
+ $adoption_agency_result = $this->run_adoption_agency_algorithm();
+ if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
+ $this->in_body_any_other_end_tag();
+ }
$this->state->active_formatting_elements->remove_node( $item );
$this->state->stack_of_open_elements->remove_node( $item );
break 2;
@@ -2894,7 +2897,10 @@ private function step_in_body(): bool {
if ( $this->state->stack_of_open_elements->has_element_in_scope( 'NOBR' ) ) {
// Parse error.
- $this->run_adoption_agency_algorithm();
+ $adoption_agency_result = $this->run_adoption_agency_algorithm();
+ if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
+ $this->in_body_any_other_end_tag();
+ }
$this->reconstruct_active_formatting_elements();
}
@@ -2920,7 +2926,10 @@ private function step_in_body(): bool {
case '-STRONG':
case '-TT':
case '-U':
- $this->run_adoption_agency_algorithm();
+ $adoption_agency_result = $this->run_adoption_agency_algorithm();
+ if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
+ $this->in_body_any_other_end_tag();
+ }
return true;
/*
@@ -3256,38 +3265,53 @@ private function step_in_body(): bool {
/*
* > Any other end tag
*/
+ return $this->in_body_any_other_end_tag();
+ }
- /*
- * Find the corresponding tag opener in the stack of open elements, if
- * it exists before reaching a special element, which provides a kind
- * of boundary in the stack. For example, a `` should not
- * close anything beyond its containing `P` or `DIV` element.
- */
- foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
- if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
- break;
- }
+ $this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
+ // This unnecessary return prevents tools from inaccurately reporting type errors.
+ return false;
+ }
- if ( self::is_special( $node ) ) {
- // This is a parse error, ignore the token.
- return $this->step();
- }
+ /**
+ * In body insertion mode's "any other end tag" logic can be invoked from different places
+ * and may require additional processing.
+ *
+ * @return bool Whether an element was found.
+ */
+ private function in_body_any_other_end_tag(): bool {
+ $token_name = $this->get_token_name();
+
+ /*
+ * Find the corresponding tag opener in the stack of open elements, if
+ * it exists before reaching a special element, which provides a kind
+ * of boundary in the stack. For example, a `` should not
+ * close anything beyond its containing `P` or `DIV` element.
+ */
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
+ if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
+ break;
}
- $this->generate_implied_end_tags( $token_name );
- if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
- // @todo Record parse error: this error doesn't impact parsing.
+ if ( self::is_special( $node ) ) {
+ // This is a parse error, ignore the token.
+ return $this->step();
}
+ }
- foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
- $this->state->stack_of_open_elements->pop();
- if ( $node === $item ) {
- return true;
- }
+ $this->generate_implied_end_tags( $token_name );
+ if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
+ // @todo Record parse error: this error doesn't impact parsing.
+ }
+
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
+ $this->state->stack_of_open_elements->pop();
+ if ( $node === $item ) {
+ return true;
}
}
- $this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
+ $this->bail( 'Should not have been able to reach end of IN BODY "any other end tag" processing. Check HTML API code.' );
// This unnecessary return prevents tools from inaccurately reporting type errors.
return false;
}
@@ -6223,8 +6247,10 @@ private function reset_insertion_mode_appropriately(): void {
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
+ *
+ * @return 'act-as-any-other-end-tag'|null Return `'act-as-any-other-end-tag'` to "act as described in the 'any other end tag' entry above."
*/
- private function run_adoption_agency_algorithm(): void {
+ private function run_adoption_agency_algorithm(): ?string {
$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();
@@ -6236,13 +6262,13 @@ private function run_adoption_agency_algorithm(): void {
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
- return;
+ return null;
}
$outer_loop_counter = 0;
while ( $budget-- > 0 ) {
if ( $outer_loop_counter++ >= 8 ) {
- return;
+ return null;
}
/*
@@ -6265,18 +6291,18 @@ private function run_adoption_agency_algorithm(): void {
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
- $this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
+ return 'act-as-any-other-end-tag';
}
// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return;
+ return null;
}
// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
- return;
+ return null;
}
/*
@@ -6312,7 +6338,7 @@ private function run_adoption_agency_algorithm(): void {
if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return;
+ return null;
}
}
}
From e3ad4fe490936c2e4d898abe6314813e08bbc942 Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Fri, 19 Jun 2026 11:32:34 +0200
Subject: [PATCH 2/6] Run "any other end tag" in AA algorithm
Simplifies return and flow, no need to leave it for caller
---
.../html-api/class-wp-html-processor.php | 32 +++++++------------
1 file changed, 11 insertions(+), 21 deletions(-)
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 2db46e04119b5..8b4457dc45757 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -2853,10 +2853,7 @@ private function step_in_body(): bool {
break 2;
case 'A':
- $adoption_agency_result = $this->run_adoption_agency_algorithm();
- if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
- $this->in_body_any_other_end_tag();
- }
+ $this->run_adoption_agency_algorithm();
$this->state->active_formatting_elements->remove_node( $item );
$this->state->stack_of_open_elements->remove_node( $item );
break 2;
@@ -2897,10 +2894,7 @@ private function step_in_body(): bool {
if ( $this->state->stack_of_open_elements->has_element_in_scope( 'NOBR' ) ) {
// Parse error.
- $adoption_agency_result = $this->run_adoption_agency_algorithm();
- if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
- $this->in_body_any_other_end_tag();
- }
+ $this->run_adoption_agency_algorithm();
$this->reconstruct_active_formatting_elements();
}
@@ -2926,10 +2920,7 @@ private function step_in_body(): bool {
case '-STRONG':
case '-TT':
case '-U':
- $adoption_agency_result = $this->run_adoption_agency_algorithm();
- if ( 'act-as-any-other-end-tag' === $adoption_agency_result ) {
- $this->in_body_any_other_end_tag();
- }
+ $this->run_adoption_agency_algorithm();
return true;
/*
@@ -6247,10 +6238,8 @@ private function reset_insertion_mode_appropriately(): void {
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
- *
- * @return 'act-as-any-other-end-tag'|null Return `'act-as-any-other-end-tag'` to "act as described in the 'any other end tag' entry above."
*/
- private function run_adoption_agency_algorithm(): ?string {
+ private function run_adoption_agency_algorithm(): void {
$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();
@@ -6262,13 +6251,13 @@ private function run_adoption_agency_algorithm(): ?string {
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
- return null;
+ return;
}
$outer_loop_counter = 0;
while ( $budget-- > 0 ) {
if ( $outer_loop_counter++ >= 8 ) {
- return null;
+ return;
}
/*
@@ -6291,18 +6280,19 @@ private function run_adoption_agency_algorithm(): ?string {
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
- return 'act-as-any-other-end-tag';
+ $this->in_body_any_other_end_tag();
+ return;
}
// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return null;
+ return;
}
// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
- return null;
+ return;
}
/*
@@ -6338,7 +6328,7 @@ private function run_adoption_agency_algorithm(): ?string {
if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return null;
+ return;
}
}
}
From c3ddcf657bd438de5973fb97bc69819562a0cc7d Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Fri, 19 Jun 2026 11:44:57 +0200
Subject: [PATCH 3/6] Avoid possible `step()` from within AA algorithm
---
.../html-api/class-wp-html-processor.php | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
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 8b4457dc45757..1ce38432598ed 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -3268,9 +3268,11 @@ private function step_in_body(): bool {
* In body insertion mode's "any other end tag" logic can be invoked from different places
* and may require additional processing.
*
+ * @param $should_step bool Set to `false` to prevent advancing the parser in case the token
+ * is ignored.
* @return bool Whether an element was found.
*/
- private function in_body_any_other_end_tag(): bool {
+ private function in_body_any_other_end_tag( bool $should_step = true ): bool {
$token_name = $this->get_token_name();
/*
@@ -3286,7 +3288,7 @@ private function in_body_any_other_end_tag(): bool {
if ( self::is_special( $node ) ) {
// This is a parse error, ignore the token.
- return $this->step();
+ return $should_step ? $this->step() : false;
}
}
@@ -6280,7 +6282,13 @@ private function run_adoption_agency_algorithm(): void {
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
- $this->in_body_any_other_end_tag();
+ /*
+ * The Adoption Agency Algorithm is not responsible for advancing the parser state,
+ * so `in_body_any_other_end_tag` is invoked with `$should_step = false` argument.
+ * This algorithm will return to the context that called it, which is responsible
+ * for advancing the parser.
+ */
+ $this->in_body_any_other_end_tag( false );
return;
}
From 71112dddd95628ab6cbfcc6ed98d4eb20a54a49f Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Fri, 19 Jun 2026 21:19:00 +0200
Subject: [PATCH 4/6] HTML API: Test unexpected formatting end tag
serialization
---
.../html-api/wpHtmlProcessor-serialize.php | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
index 5afe37a010a41..c95d1345750c6 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
@@ -265,6 +265,47 @@ public function test_unexpected_closing_tags_are_removed() {
);
}
+ /**
+ * Ensures that unexpected closing formatting tags are ignored.
+ *
+ * @ticket 65372
+ *
+ * @dataProvider data_unexpected_closing_formatting_tags
+ *
+ * @param string $formatting_tag_name Formatting tag name with no active formatting element.
+ */
+ public function test_unexpected_closing_formatting_tags_are_ignored( string $formatting_tag_name ) {
+ $this->assertSame(
+ 'onetwo',
+ WP_HTML_Processor::normalize( "one{$formatting_tag_name}>two" ),
+ "Should have ignored unexpected {$formatting_tag_name} closer."
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public static function data_unexpected_closing_formatting_tags() {
+ return array(
+ 'Unexpected A end tag' => array( 'a' ),
+ 'Unexpected B end tag' => array( 'b' ),
+ 'Unexpected BIG end tag' => array( 'big' ),
+ 'Unexpected CODE end tag' => array( 'code' ),
+ 'Unexpected EM end tag' => array( 'em' ),
+ 'Unexpected FONT end tag' => array( 'font' ),
+ 'Unexpected I end tag' => array( 'i' ),
+ 'Unexpected NOBR end tag' => array( 'nobr' ),
+ 'Unexpected S end tag' => array( 's' ),
+ 'Unexpected SMALL end tag' => array( 'small' ),
+ 'Unexpected STRIKE end tag' => array( 'strike' ),
+ 'Unexpected STRONG end tag' => array( 'strong' ),
+ 'Unexpected TT end tag' => array( 'tt' ),
+ 'Unexpected U end tag' => array( 'u' ),
+ );
+ }
+
/**
* Ensures that self-closing elements in foreign content retain their self-closing flag.
*
From 97358f2fb6b362be789f60dddccfa5bd5eabb71f Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Fri, 19 Jun 2026 21:22:59 +0200
Subject: [PATCH 5/6] HTML API: Preserve adoption agency fallback step result
---
.../html-api/class-wp-html-processor.php | 45 ++++++------
.../html-api/wpHtmlProcessorBreadcrumbs.php | 1 +
.../html-api/wpHtmlProcessorSemanticRules.php | 72 +++++++++++++++++++
3 files changed, 96 insertions(+), 22 deletions(-)
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 1ce38432598ed..7e277120511c8 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -2920,7 +2920,9 @@ private function step_in_body(): bool {
case '-STRONG':
case '-TT':
case '-U':
- $this->run_adoption_agency_algorithm();
+ if ( $this->run_adoption_agency_algorithm() ) {
+ return $this->step_in_body_any_other_end_tag();
+ }
return true;
/*
@@ -3256,7 +3258,7 @@ private function step_in_body(): bool {
/*
* > Any other end tag
*/
- return $this->in_body_any_other_end_tag();
+ return $this->step_in_body_any_other_end_tag();
}
$this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
@@ -3265,14 +3267,16 @@ private function step_in_body(): bool {
}
/**
- * In body insertion mode's "any other end tag" logic can be invoked from different places
- * and may require additional processing.
+ * Parses an "any other end tag" token in the "in body" insertion mode.
+ *
+ * @since 7.1.0
+ * @ignore
+ *
+ * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
- * @param $should_step bool Set to `false` to prevent advancing the parser in case the token
- * is ignored.
* @return bool Whether an element was found.
*/
- private function in_body_any_other_end_tag( bool $should_step = true ): bool {
+ private function step_in_body_any_other_end_tag(): bool {
$token_name = $this->get_token_name();
/*
@@ -3288,7 +3292,7 @@ private function in_body_any_other_end_tag( bool $should_step = true ): bool {
if ( self::is_special( $node ) ) {
// This is a parse error, ignore the token.
- return $should_step ? $this->step() : false;
+ return $this->step();
}
}
@@ -6240,8 +6244,10 @@ private function reset_insertion_mode_appropriately(): void {
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
+ *
+ * @return bool Whether the caller should process the current end tag as "any other end tag".
*/
- private function run_adoption_agency_algorithm(): void {
+ private function run_adoption_agency_algorithm(): bool {
$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();
@@ -6253,13 +6259,13 @@ private function run_adoption_agency_algorithm(): void {
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
- return;
+ return false;
}
$outer_loop_counter = 0;
while ( $budget-- > 0 ) {
if ( $outer_loop_counter++ >= 8 ) {
- return;
+ return false;
}
/*
@@ -6282,25 +6288,18 @@ private function run_adoption_agency_algorithm(): void {
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
- /*
- * The Adoption Agency Algorithm is not responsible for advancing the parser state,
- * so `in_body_any_other_end_tag` is invoked with `$should_step = false` argument.
- * This algorithm will return to the context that called it, which is responsible
- * for advancing the parser.
- */
- $this->in_body_any_other_end_tag( false );
- return;
+ return true;
}
// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return;
+ return false;
}
// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
- return;
+ return false;
}
/*
@@ -6336,7 +6335,7 @@ private function run_adoption_agency_algorithm(): void {
if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
- return;
+ return false;
}
}
}
@@ -6345,6 +6344,8 @@ private function run_adoption_agency_algorithm(): void {
}
$this->bail( 'Cannot run adoption agency when looping required.' );
+ // This unnecessary return prevents tools from inaccurately reporting type errors.
+ return false;
}
/**
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
index b54fc047ab040..085c409e46d0c 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
@@ -577,6 +577,7 @@ public static function data_virtual_nodes_breadcrumbs() {
'Implied P tag opener on unmatched closer' => array( '
', 1, 'P', 'open', array( 'HTML', 'BODY', 'P' ) ),
'Implied heading tag closer on heading child' => array( '