Skip to content

Commit 902d035

Browse files
committed
Merge commit '5ad4fd6' into html-api/add-get-set-inner-outer-contents
2 parents b1d5926 + 5ad4fd6 commit 902d035

6 files changed

Lines changed: 191 additions & 26 deletions

File tree

src/wp-includes/html-api/class-wp-html-open-elements.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ public function has_element_in_specific_scope( $tag_name, $termination_list ) {
115115
if ( $node->node_name === $tag_name ) {
116116
return true;
117117
}
118+
119+
switch ( $node->node_name ) {
120+
case 'HTML':
121+
return false;
122+
}
123+
124+
if ( in_array( $node->node_name, $termination_list, true ) ) {
125+
return true;
126+
}
118127
}
119128

120129
return false;
@@ -175,19 +184,7 @@ public function has_element_in_list_item_scope( $tag_name ) { // phpcs:ignore Va
175184
* @return bool Whether given element is in scope.
176185
*/
177186
public function has_element_in_button_scope( $tag_name ) {
178-
return $this->has_element_in_specific_scope(
179-
$tag_name,
180-
array(
181-
182-
/*
183-
* Because it's not currently possible to encounter
184-
* one of the termination elements, they don't need
185-
* to be listed here. If they were, they would be
186-
* unreachable and only waste CPU cycles while
187-
* scanning through HTML.
188-
*/
189-
)
190-
);
187+
return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) );
191188
}
192189

193190
/**
@@ -394,6 +391,10 @@ public function after_element_push( $item ) {
394391
* cases where the precalculated value needs to change.
395392
*/
396393
switch ( $item->node_name ) {
394+
case 'BUTTON':
395+
$this->has_p_in_button_scope = false;
396+
break;
397+
397398
case 'P':
398399
$this->has_p_in_button_scope = true;
399400
break;
@@ -419,6 +420,10 @@ public function after_element_pop( $item ) {
419420
* cases where the precalculated value needs to change.
420421
*/
421422
switch ( $item->node_name ) {
423+
case 'BUTTON':
424+
$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
425+
break;
426+
422427
case 'P':
423428
$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
424429
break;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ class WP_HTML_Processor_State {
107107
*/
108108
public $context_node = null;
109109

110+
/**
111+
* The frameset-ok flag indicates if a `FRAMESET` element is allowed in the current state.
112+
*
113+
* > The frameset-ok flag is set to "ok" when the parser is created. It is set to "not ok" after certain tokens are seen.
114+
*
115+
* @since 6.4.0
116+
*
117+
* @see https://html.spec.whatwg.org/#frameset-ok-flag
118+
*
119+
* @var bool
120+
*/
121+
public $frameset_ok = true;
122+
110123
/**
111124
* Constructor - creates a new and empty state value.
112125
*

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,13 @@ public function get_last_error() {
349349
*/
350350
public function next_tag( $query = null ) {
351351
if ( null === $query ) {
352-
return $this->step();
352+
while ( $this->step() ) {
353+
if ( ! $this->is_tag_closer() ) {
354+
return true;
355+
}
356+
}
357+
358+
return false;
353359
}
354360

355361
if ( is_string( $query ) ) {
@@ -366,7 +372,13 @@ public function next_tag( $query = null ) {
366372
}
367373

368374
if ( ! ( array_key_exists( 'breadcrumbs', $query ) && is_array( $query['breadcrumbs'] ) ) ) {
369-
return $this->step();
375+
while ( $this->step() ) {
376+
if ( ! $this->is_tag_closer() ) {
377+
return true;
378+
}
379+
}
380+
381+
return false;
370382
}
371383

372384
if ( isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'] ) {
@@ -383,7 +395,7 @@ public function next_tag( $query = null ) {
383395

384396
$crumb = end( $breadcrumbs );
385397
$target = strtoupper( $crumb );
386-
while ( $this->step() ) {
398+
while ( $match_offset > 0 && $this->step() ) {
387399
if ( $target !== $this->get_tag() ) {
388400
continue;
389401
}
@@ -395,7 +407,7 @@ public function next_tag( $query = null ) {
395407
}
396408

397409
$crumb = prev( $breadcrumbs );
398-
if ( false === $crumb && 0 === --$match_offset ) {
410+
if ( false === $crumb && 0 === --$match_offset && ! $this->is_tag_closer() ) {
399411
return true;
400412
}
401413
}
@@ -510,6 +522,22 @@ private function step_in_body() {
510522
$op = "{$op_sigil}{$tag_name}";
511523

512524
switch ( $op ) {
525+
/*
526+
* > A start tag whose tag name is "button"
527+
*/
528+
case '+BUTTON':
529+
if ( $this->state->stack_of_open_elements->has_element_in_scope( 'BUTTON' ) ) {
530+
// @TODO: Indicate a parse error once it's possible. This error does not impact the logic here.
531+
$this->generate_implied_end_tags();
532+
$this->state->stack_of_open_elements->pop_until( 'BUTTON' );
533+
}
534+
535+
$this->reconstruct_active_formatting_elements();
536+
$this->insert_html_element( $this->current_token );
537+
$this->state->frameset_ok = false;
538+
539+
return true;
540+
513541
/*
514542
* > A start tag whose tag name is one of: "address", "article", "aside",
515543
* > "blockquote", "center", "details", "dialog", "dir", "div", "dl",
@@ -535,15 +563,20 @@ private function step_in_body() {
535563
* > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
536564
*/
537565
case '-BLOCKQUOTE':
566+
case '-BUTTON':
538567
case '-DIV':
539568
case '-FIGCAPTION':
540569
case '-FIGURE':
541570
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) {
571+
// @TODO: Report parse error.
542572
// Ignore the token.
543573
return $this->step();
544574
}
545575

546576
$this->generate_implied_end_tags();
577+
if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) {
578+
// @TODO: Record parse error: this error doesn't impact parsing.
579+
}
547580
$this->state->stack_of_open_elements->pop_until( $tag_name );
548581
return true;
549582

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function data_single_tag_of_supported_elements() {
3939
'A',
4040
'B',
4141
'BIG',
42+
'BUTTON',
4243
'CODE',
4344
'DIV',
4445
'EM',
@@ -111,7 +112,6 @@ public function data_unsupported_elements() {
111112
'BLINK', // Deprecated
112113
'BODY',
113114
'BR',
114-
'BUTTON',
115115
'CANVAS',
116116
'CAPTION',
117117
'CENTER', // Neutralized

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

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,127 @@ class Tests_HtmlApi_WpHtmlProcessorSemanticRules extends WP_UnitTestCase {
1616
* RULES FOR "IN BODY" MODE
1717
*******************************************************************/
1818

19+
/**
20+
* Verifies that when encountering an end tag for which there is no corresponding
21+
* element in scope, that it skips the tag entirely.
22+
*
23+
* @ticket 58961
24+
*
25+
* @since 6.4.0
26+
*
27+
* @throws Exception
28+
*/
29+
public function test_in_body_skips_unexpected_button_closer() {
30+
$p = WP_HTML_Processor::createFragment( '<div>Test</button></div>' );
31+
32+
$p->step();
33+
$this->assertEquals( 'DIV', $p->get_tag(), 'Did not stop at initial DIV tag.' );
34+
$this->assertFalse( $p->is_tag_closer(), 'Did not find that initial DIV tag is an opener.' );
35+
36+
/*
37+
* When encountering the BUTTON closing tag, there is no BUTTON in the stack of open elements.
38+
* It should be ignored as there's no BUTTON to close.
39+
*/
40+
$this->assertTrue( $p->step(), 'Found no further tags when it should have found the closing DIV' );
41+
$this->assertEquals( 'DIV', $p->get_tag(), "Did not skip unexpected BUTTON; stopped at {$p->get_tag()}." );
42+
$this->assertTrue( $p->is_tag_closer(), 'Did not find that the terminal DIV tag is a closer.' );
43+
}
44+
45+
/**
46+
* Verifies insertion of a BUTTON element when no existing BUTTON is already in scope.
47+
*
48+
* @ticket 58961
49+
*
50+
* @since 6.4.0
51+
*
52+
* @throws WP_HTML_Unsupported_Exception
53+
*/
54+
public function test_in_body_button_with_no_button_in_scope() {
55+
$p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>here</button>!</p></div><button two>not here</button>' );
56+
57+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
58+
$this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
59+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
60+
61+
/*
62+
* There's nothing special about this HTML construction, but it's important to verify that
63+
* the HTML Processor can find a BUTTON under normal and normative scenarios, not just the
64+
* malformed and unexpected ones.
65+
*/
66+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
67+
$this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
68+
$this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
69+
}
70+
71+
/**
72+
* Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
73+
* that the open button is closed with all other elements inside of it.
74+
*
75+
* @ticket 58961
76+
*
77+
* @since 6.4.0
78+
*
79+
* @throws WP_HTML_Unsupported_Exception
80+
*/
81+
public function test_in_body_button_with_button_in_scope_as_parent() {
82+
$p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>almost<button two>here</button>!</p></div><button three>not here</button>' );
83+
84+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
85+
$this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
86+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
87+
88+
/*
89+
* A naive parser might skip the second BUTTON because it's looking for the close of the first one,
90+
* or it may place it as a child of the first one, but it implicitly closes the open BUTTON.
91+
*/
92+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
93+
$this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
94+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
95+
96+
/*
97+
* This is another form of the test for the second button, but from a different side. The test is
98+
* looking for proper handling of the open and close sequence for the BUTTON tags.
99+
*/
100+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
101+
$this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
102+
$this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
103+
}
104+
105+
/**
106+
* Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
107+
* that the open button is closed with all other elements inside of it, even if the
108+
* BUTTON in scope is not a direct parent of the new BUTTON element.
109+
*
110+
* @ticket 58961
111+
*
112+
* @since 6.4.0
113+
*
114+
* @throws WP_HTML_Unsupported_Exception
115+
*/
116+
public function test_in_body_button_with_button_in_scope_as_ancestor() {
117+
$p = WP_HTML_Processor::createFragment( '<div><button one><p>Click the button <span><button two>here</button>!</span></p></div><button three>not here</button>' );
118+
119+
// This button finds itself normally nesting inside the DIV.
120+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
121+
$this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
122+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
123+
124+
/*
125+
* Because the second button appears while a BUTTON is in scope, it generates implied end tags and closes
126+
* the BUTTON, P, and SPAN elements. It looks like the BUTTON is inside the SPAN, but it's another case
127+
* of an unexpected closing SPAN tag because the SPAN was closed by the second BUTTON. This element finds
128+
* itself a child of the most-recent open element above the most-recent BUTTON, or the DIV.
129+
*/
130+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
131+
$this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
132+
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
133+
134+
// The third button is back to normal, because everything has been implicitly or explicitly closed by now.
135+
$this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
136+
$this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
137+
$this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
138+
}
139+
19140
/*
20141
* Verifies that when "in body" and encountering "any other end tag"
21142
* that the HTML processor ignores the end tag if there's a special
@@ -57,7 +178,7 @@ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element
57178
$this->assertSame( 'CODE', $p->get_tag(), "Expected to start test on CODE element but found {$p->get_tag()} instead." );
58179
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
59180

60-
$this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' );
181+
$this->assertTrue( $p->step(), 'Failed to advance past CODE tag to expected SPAN closer.' );
61182
$this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' );
62183
$this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' );
63184

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,6 @@ public function test_has_element_in_button_scope_needs_support() {
176176
$this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
177177
$this->ensure_support_is_added_everywhere( 'DESC' );
178178
$this->ensure_support_is_added_everywhere( 'TITLE' );
179-
180-
$this->ensure_support_is_added_everywhere( 'BUTTON' );
181179
}
182180

183181
/**
@@ -218,9 +216,6 @@ public function test_after_element_pop_must_maintain_p_in_button_scope_flag() {
218216
$this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
219217
$this->ensure_support_is_added_everywhere( 'DESC' );
220218
$this->ensure_support_is_added_everywhere( 'TITLE' );
221-
222-
// This element is specific to BUTTON scope.
223-
$this->ensure_support_is_added_everywhere( 'BUTTON' );
224219
}
225220

226221
/**
@@ -261,8 +256,6 @@ public function test_after_element_push_must_maintain_p_in_button_scope_flag() {
261256
$this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
262257
$this->ensure_support_is_added_everywhere( 'DESC' );
263258
$this->ensure_support_is_added_everywhere( 'TITLE' );
264-
265-
$this->ensure_support_is_added_everywhere( 'BUTTON' );
266259
}
267260

268261
/**

0 commit comments

Comments
 (0)