Skip to content

Commit f5caa48

Browse files
committed
Merge PR #44: HTML API: Handle adoption agency fallback end tags
# Conflicts: # src/wp-includes/html-api/class-wp-html-processor.php
2 parents 00600da + 579ad58 commit f5caa48

3 files changed

Lines changed: 144 additions & 32 deletions

File tree

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

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,8 +3047,7 @@ private function step_in_body(): bool {
30473047
case '-STRONG':
30483048
case '-TT':
30493049
case '-U':
3050-
$this->run_adoption_agency_algorithm();
3051-
return true;
3050+
return $this->run_adoption_agency_algorithm();
30523051

30533052
/*
30543053
* > A start tag whose tag name is one of: "applet", "marquee", "object"
@@ -3469,38 +3468,57 @@ private function step_in_body(): bool {
34693468
*
34703469
* The "maybe clone an option into selectedcontent" algorithm is not implemented.
34713470
*/
3471+
return $this->step_in_body_any_other_end_tag();
3472+
}
34723473

3473-
/*
3474-
* Find the corresponding tag opener in the stack of open elements, if
3475-
* it exists before reaching a special element, which provides a kind
3476-
* of boundary in the stack. For example, a `</custom-tag>` should not
3477-
* close anything beyond its containing `P` or `DIV` element.
3478-
*/
3479-
foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
3480-
if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
3481-
break;
3482-
}
3474+
$this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
3475+
// This unnecessary return prevents tools from inaccurately reporting type errors.
3476+
return false;
3477+
}
34833478

3484-
if ( self::is_special( $node ) ) {
3485-
// This is a parse error, ignore the token.
3486-
return $this->step();
3487-
}
3479+
/**
3480+
* Parses an "any other end tag" token in the "in body" insertion mode.
3481+
*
3482+
* @since 7.1.0
3483+
* @ignore
3484+
*
3485+
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3486+
*
3487+
* @return bool Whether an element was found.
3488+
*/
3489+
private function step_in_body_any_other_end_tag(): bool {
3490+
$token_name = $this->get_token_name();
3491+
3492+
/*
3493+
* Find the corresponding tag opener in the stack of open elements, if
3494+
* it exists before reaching a special element, which provides a kind
3495+
* of boundary in the stack. For example, a `</custom-tag>` should not
3496+
* close anything beyond its containing `P` or `DIV` element.
3497+
*/
3498+
foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
3499+
if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
3500+
break;
34883501
}
34893502

3490-
$this->generate_implied_end_tags( $token_name );
3491-
if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
3492-
// @todo Record parse error: this error doesn't impact parsing.
3503+
if ( self::is_special( $node ) ) {
3504+
// This is a parse error, ignore the token.
3505+
return $this->step();
34933506
}
3507+
}
34943508

3495-
foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
3496-
$this->state->stack_of_open_elements->pop();
3497-
if ( $node === $item ) {
3498-
return true;
3499-
}
3509+
$this->generate_implied_end_tags( $token_name );
3510+
if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
3511+
// @todo Record parse error: this error doesn't impact parsing.
3512+
}
3513+
3514+
foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
3515+
$this->state->stack_of_open_elements->pop();
3516+
if ( $node === $item ) {
3517+
return true;
35003518
}
35013519
}
35023520

3503-
$this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
3521+
$this->bail( 'Should not have been able to reach end of IN BODY "any other end tag" processing. Check HTML API code.' );
35043522
// This unnecessary return prevents tools from inaccurately reporting type errors.
35053523
return false;
35063524
}
@@ -6147,8 +6165,10 @@ private function reset_insertion_mode_appropriately(): void {
61476165
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
61486166
*
61496167
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
6168+
*
6169+
* @return bool Whether an element was found.
61506170
*/
6151-
private function run_adoption_agency_algorithm(): void {
6171+
private function run_adoption_agency_algorithm(): bool {
61526172
$budget = 1000;
61536173
$subject = $this->get_tag();
61546174
$current_node = $this->state->stack_of_open_elements->current_node();
@@ -6160,13 +6180,13 @@ private function run_adoption_agency_algorithm(): void {
61606180
! $this->state->active_formatting_elements->contains_node( $current_node )
61616181
) {
61626182
$this->state->stack_of_open_elements->pop();
6163-
return;
6183+
return true;
61646184
}
61656185

61666186
$outer_loop_counter = 0;
61676187
while ( $budget-- > 0 ) {
61686188
if ( $outer_loop_counter++ >= 8 ) {
6169-
return;
6189+
return true;
61706190
}
61716191

61726192
/*
@@ -6189,18 +6209,18 @@ private function run_adoption_agency_algorithm(): void {
61896209

61906210
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
61916211
if ( null === $formatting_element ) {
6192-
$this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
6212+
return $this->step_in_body_any_other_end_tag();
61936213
}
61946214

61956215
// > 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.
61966216
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
61976217
$this->state->active_formatting_elements->remove_node( $formatting_element );
6198-
return;
6218+
return true;
61996219
}
62006220

62016221
// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
62026222
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
6203-
return;
6223+
return true;
62046224
}
62056225

62066226
/*
@@ -6236,7 +6256,7 @@ private function run_adoption_agency_algorithm(): void {
62366256

62376257
if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
62386258
$this->state->active_formatting_elements->remove_node( $formatting_element );
6239-
return;
6259+
return true;
62406260
}
62416261
}
62426262
}
@@ -6245,6 +6265,8 @@ private function run_adoption_agency_algorithm(): void {
62456265
}
62466266

62476267
$this->bail( 'Cannot run adoption agency when looping required.' );
6268+
// This unnecessary return prevents tools from inaccurately reporting type errors.
6269+
return false;
62486270
}
62496271

62506272
/**

tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,36 @@ public function test_unexpected_closing_tags_are_removed() {
278278
);
279279
}
280280

281+
/**
282+
* Ensures that unexpected closing formatting tags are ignored.
283+
*
284+
* @ticket 65372
285+
*
286+
* @dataProvider data_unexpected_closing_formatting_tags
287+
*
288+
* @param string $html HTML containing an unexpected closing formatting tag.
289+
* @param string $expected Expected normalized output.
290+
*/
291+
public function test_unexpected_closing_formatting_tags_are_ignored( string $html, string $expected ) {
292+
$this->assertSame(
293+
$expected,
294+
WP_HTML_Processor::normalize( $html ),
295+
'Should have ignored unexpected closing formatting tags.'
296+
);
297+
}
298+
299+
/**
300+
* Data provider.
301+
*
302+
* @return array[]
303+
*/
304+
public static function data_unexpected_closing_formatting_tags() {
305+
return array(
306+
'Unexpected A end tag' => array( 'one</a>two', 'onetwo' ),
307+
'Unexpected B end tag' => array( 'one</b>two', 'onetwo' ),
308+
);
309+
}
310+
281311
/**
282312
* Ensures that self-closing elements in foreign content retain their self-closing flag.
283313
*

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,66 @@ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element
405405
$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.' );
406406
}
407407

408+
/**
409+
* Verifies that when the adoption agency algorithm finds no matching
410+
* active formatting element, it acts like "any other end tag".
411+
*
412+
* @covers WP_HTML_Processor::step_in_body
413+
*
414+
* @ticket 65372
415+
*
416+
* @dataProvider data_in_body_adoption_agency_falls_back_to_any_other_end_tag
417+
*
418+
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
419+
*/
420+
public function test_in_body_adoption_agency_falls_back_to_any_other_end_tag( string $formatting_tag_name ) {
421+
$processor = WP_HTML_Processor::create_fragment( "<div><span></{$formatting_tag_name}><code target></code></span></div>" );
422+
423+
$processor->next_tag( 'SPAN' );
424+
$this->assertSame( 'SPAN', $processor->get_tag(), "Expected to start test on SPAN element but found {$processor->get_tag()} instead." );
425+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN' ), $processor->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
426+
427+
$this->assertTrue( $processor->next_tag( 'CODE' ), "Failed to ignore unexpected {$formatting_tag_name} closer and advance to CODE opener." );
428+
$this->assertSame( 'CODE', $processor->get_tag(), "Expected to find CODE element, but found {$processor->get_tag()} instead." );
429+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $processor->get_breadcrumbs(), 'Failed to keep SPAN open after unexpected formatting closer.' );
430+
}
431+
432+
/**
433+
* Verifies that the adoption agency fallback preserves the "any other end tag"
434+
* step result when the ignored token is followed by EOF.
435+
*
436+
* @covers WP_HTML_Processor::step_in_body
437+
*
438+
* @ticket 65372
439+
*
440+
* @dataProvider data_in_body_adoption_agency_falls_back_to_any_other_end_tag
441+
*
442+
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
443+
*/
444+
public function test_in_body_adoption_agency_fallback_preserves_any_other_end_tag_step_result( string $formatting_tag_name ) {
445+
$ordinary_processor = WP_HTML_Processor::create_fragment( '<span></x>' );
446+
$this->assertTrue( $ordinary_processor->step(), 'Failed to find the SPAN opener before an ordinary unexpected end tag.' );
447+
$this->assertSame( 'SPAN', $ordinary_processor->get_tag(), "Expected to start test on SPAN element but found {$ordinary_processor->get_tag()} instead." );
448+
$this->assertFalse( $ordinary_processor->step(), 'Expected ordinary unexpected end tag followed by EOF to return false.' );
449+
450+
$formatting_processor = WP_HTML_Processor::create_fragment( "<span></{$formatting_tag_name}>" );
451+
$this->assertTrue( $formatting_processor->step(), 'Failed to find the SPAN opener before an unexpected formatting end tag.' );
452+
$this->assertSame( 'SPAN', $formatting_processor->get_tag(), "Expected to start test on SPAN element but found {$formatting_processor->get_tag()} instead." );
453+
$this->assertFalse( $formatting_processor->step(), 'Expected unexpected formatting end tag followed by EOF to return false.' );
454+
}
455+
456+
/**
457+
* Data provider.
458+
*
459+
* @return array[]
460+
*/
461+
public static function data_in_body_adoption_agency_falls_back_to_any_other_end_tag() {
462+
return array(
463+
'Unexpected A end tag' => array( 'a' ),
464+
'Unexpected B end tag' => array( 'b' ),
465+
);
466+
}
467+
408468
/**
409469
* Ensures that closing `</br>` tags are appropriately treated as opening tags with no attributes.
410470
*

0 commit comments

Comments
 (0)