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( "onetwo" ), + "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( '

', 2, 'H1', 'close', array( 'HTML', 'BODY' ) ), 'Implied A tag closer on A tag child' => array( '', 2, 'A', 'close', array( 'HTML', 'BODY' ) ), + 'Explicit A closer after sibling A' => array( '', 4, 'A', 'close', array( 'HTML', 'BODY' ) ), 'Implied A tag closer on A tag descendent' => array( '', 4, 'A', 'close', array( 'HTML', 'BODY' ) ), ); } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php index da6d959eb75e0..9d24ac5bbfb09 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php @@ -405,6 +405,78 @@ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'DIV' ), $processor->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should be closed and DIV should be its sibling.' ); } + /** + * Verifies that when the adoption agency algorithm finds no matching + * active formatting element, it acts like "any other end tag". + * + * @covers WP_HTML_Processor::step_in_body + * + * @ticket 65372 + * + * @dataProvider data_in_body_adoption_agency_fallback_end_tags + * + * @param string $formatting_tag_name Formatting tag name with no active formatting element. + */ + public function test_in_body_adoption_agency_fallback_ignores_unexpected_formatting_end_tag( string $formatting_tag_name ) { + $processor = WP_HTML_Processor::create_fragment( "
" ); + + $this->assertTrue( $processor->next_tag( 'SPAN' ), 'Failed to find the SPAN opener before an unexpected formatting end tag.' ); + $this->assertSame( 'SPAN', $processor->get_tag(), "Expected to start test on SPAN element but found {$processor->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN' ), $processor->get_breadcrumbs(), 'Failed to produce expected DOM nesting before unexpected formatting closer.' ); + + $this->assertTrue( $processor->next_tag( 'CODE' ), "Failed to ignore unexpected {$formatting_tag_name} closer and advance to CODE opener." ); + $this->assertSame( 'CODE', $processor->get_tag(), "Expected to find CODE element, but found {$processor->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $processor->get_breadcrumbs(), 'Failed to keep SPAN open after unexpected formatting closer.' ); + } + + /** + * Verifies that the adoption agency fallback preserves the "any other end tag" + * step result when the ignored token is followed by EOF. + * + * @covers WP_HTML_Processor::step_in_body + * + * @ticket 65372 + * + * @dataProvider data_in_body_adoption_agency_fallback_end_tags + * + * @param string $formatting_tag_name Formatting tag name with no active formatting element. + */ + public function test_in_body_adoption_agency_fallback_preserves_ignored_end_tag_step_result( string $formatting_tag_name ) { + $ordinary_processor = WP_HTML_Processor::create_fragment( '' ); + $this->assertTrue( $ordinary_processor->step(), 'Failed to find the SPAN opener before an ordinary unexpected end tag.' ); + $this->assertSame( 'SPAN', $ordinary_processor->get_tag(), "Expected to start test on SPAN element but found {$ordinary_processor->get_tag()} instead." ); + $this->assertFalse( $ordinary_processor->step(), 'Expected ordinary unexpected end tag followed by EOF to return false.' ); + + $formatting_processor = WP_HTML_Processor::create_fragment( "" ); + $this->assertTrue( $formatting_processor->step(), 'Failed to find the SPAN opener before an unexpected formatting end tag.' ); + $this->assertSame( 'SPAN', $formatting_processor->get_tag(), "Expected to start test on SPAN element but found {$formatting_processor->get_tag()} instead." ); + $this->assertFalse( $formatting_processor->step(), "Expected unexpected {$formatting_tag_name} end tag followed by EOF to return false." ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_in_body_adoption_agency_fallback_end_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 closing `
` tags are appropriately treated as opening tags with no attributes. * From ab5854f16d070c774a515bd5abd5a6500e9c9c18 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Sun, 21 Jun 2026 13:45:07 +0200 Subject: [PATCH 6/6] Revert implementation changes to AAA --- .../html-api/class-wp-html-processor.php | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 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 7e277120511c8..1ce38432598ed 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -2920,9 +2920,7 @@ private function step_in_body(): bool { case '-STRONG': case '-TT': case '-U': - if ( $this->run_adoption_agency_algorithm() ) { - return $this->step_in_body_any_other_end_tag(); - } + $this->run_adoption_agency_algorithm(); return true; /* @@ -3258,7 +3256,7 @@ private function step_in_body(): bool { /* * > Any other end tag */ - return $this->step_in_body_any_other_end_tag(); + return $this->in_body_any_other_end_tag(); } $this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' ); @@ -3267,16 +3265,14 @@ private function step_in_body(): bool { } /** - * 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. + * 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 step_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(); /* @@ -3292,7 +3288,7 @@ private function step_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; } } @@ -6244,10 +6240,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 bool Whether the caller should process the current end tag as "any other end tag". */ - private function run_adoption_agency_algorithm(): bool { + private function run_adoption_agency_algorithm(): void { $budget = 1000; $subject = $this->get_tag(); $current_node = $this->state->stack_of_open_elements->current_node(); @@ -6259,13 +6253,13 @@ private function run_adoption_agency_algorithm(): bool { ! $this->state->active_formatting_elements->contains_node( $current_node ) ) { $this->state->stack_of_open_elements->pop(); - return false; + return; } $outer_loop_counter = 0; while ( $budget-- > 0 ) { if ( $outer_loop_counter++ >= 8 ) { - return false; + return; } /* @@ -6288,18 +6282,25 @@ private function run_adoption_agency_algorithm(): bool { // > 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 true; + /* + * 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; } // > 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 false; + 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 false; + return; } /* @@ -6335,7 +6336,7 @@ private function run_adoption_agency_algorithm(): bool { if ( $formatting_element->bookmark_name === $item->bookmark_name ) { $this->state->active_formatting_elements->remove_node( $formatting_element ); - return false; + return; } } } @@ -6344,8 +6345,6 @@ private function run_adoption_agency_algorithm(): bool { } $this->bail( 'Cannot run adoption agency when looping required.' ); - // This unnecessary return prevents tools from inaccurately reporting type errors. - return false; } /**