<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56380] trunk: HTML API: Add support for BUTTON element.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56380">56380</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56380","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>Bernhard Reiter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-08-10 08:35:55 +0000 (Thu, 10 Aug 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>HTML API: Add support for BUTTON element.

This patch adds support to process the BUTTON element. This requires adding some additional semantic rules to handle situations where a BUTTON element is already in scope.

Also included is a fixup to enforce that `WP_HTML_Processor::next_tag()` never returns for a tag closer. This is useful with the Tag Processor, but not for the HTML Processor. There were tests relying on this behavior to assert that internal processes were working as they should, but those tests have been updated to use the semi-private `step()` function, which does stop on tag closers.

This patch is one in a series of changes to expand support within the HTML API, moving gradually to allow for more focused changes that are easier to review and test. The HTML Processor is a work in progress with a certain set of features slated to be ready and tested by 6.4.0, but it will only contain partial support of the HTML5 specification even after that. Whenever it cannot positively recognize and process its input it bails, and certain function stubs and logical stubs exist to structure future expansions of support.

Props dmsnell.
Fixes <a href="https://core.trac.wordpress.org/ticket/58961">#58961</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp">trunk/src/wp-includes/html-api/class-wp-html-open-elements.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorstatephp">trunk/src/wp-includes/html-api/class-wp-html-processor-state.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorBreadcrumbsphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorSemanticRulesphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlSupportRequiredOpenElementsphp">trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-open-elements.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-open-elements.php    2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/src/wp-includes/html-api/class-wp-html-open-elements.php      2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,6 +115,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $node->node_name === $tag_name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                return true;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       switch ( $node->node_name ) {
+                               case 'HTML':
+                                       return false;
+                       }
+
+                       if ( in_array( $node->node_name, $termination_list, true ) ) {
+                               return true;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return false;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -175,19 +184,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool Whether given element is in scope.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function has_element_in_button_scope( $tag_name ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->has_element_in_specific_scope(
-                       $tag_name,
-                       array(
-
-                               /*
-                                * Because it's not currently possible to encounter
-                                * one of the termination elements, they don't need
-                                * to be listed here. If they were, they would be
-                                * unreachable and only waste CPU cycles while
-                                * scanning through HTML.
-                                */
-                       )
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -394,6 +391,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * cases where the precalculated value needs to change.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                switch ( $item->node_name ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        case 'BUTTON':
+                               $this->has_p_in_button_scope = false;
+                               break;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         case 'P':
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->has_p_in_button_scope = true;
</span><span class="cx" style="display: block; padding: 0 10px">                                break;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -419,6 +420,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * cases where the precalculated value needs to change.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                switch ( $item->node_name ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        case 'BUTTON':
+                               $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
+                               break;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         case 'P':
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
</span><span class="cx" style="display: block; padding: 0 10px">                                break;
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorstatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor-state.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor-state.php  2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor-state.php    2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -108,6 +108,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public $context_node = null;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * The frameset-ok flag indicates if a `FRAMESET` element is allowed in the current state.
+        *
+        * > The frameset-ok flag is set to "ok" when the parser is created. It is set to "not ok" after certain tokens are seen.
+        *
+        * @since 6.4.0
+        *
+        * @see https://html.spec.whatwg.org/#frameset-ok-flag
+        *
+        * @var bool
+        */
+       public $frameset_ok = true;
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Constructor - creates a new and empty state value.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.4.0
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor.php        2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -349,7 +349,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function next_tag( $query = null ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( null === $query ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return $this->step();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 while ( $this->step() ) {
+                               if ( ! $this->is_tag_closer() ) {
+                                       return true;
+                               }
+                       }
+
+                       return false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_string( $query ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -366,7 +372,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! ( array_key_exists( 'breadcrumbs', $query ) && is_array( $query['breadcrumbs'] ) ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return $this->step();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 while ( $this->step() ) {
+                               if ( ! $this->is_tag_closer() ) {
+                                       return true;
+                               }
+                       }
+
+                       return false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'] ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -383,7 +395,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $crumb  = end( $breadcrumbs );
</span><span class="cx" style="display: block; padding: 0 10px">                $target = strtoupper( $crumb );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                while ( $this->step() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         while ( $match_offset > 0 && $this->step() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $target !== $this->get_tag() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                continue;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -395,7 +407,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                $crumb = prev( $breadcrumbs );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( false === $crumb && 0 === --$match_offset ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( false === $crumb && 0 === --$match_offset && ! $this->is_tag_closer() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         return true;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -511,6 +523,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                switch ( $op ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        /*
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         * > A start tag whose tag name is "button"
+                        */
+                       case '+BUTTON':
+                               if ( $this->state->stack_of_open_elements->has_element_in_scope( 'BUTTON' ) ) {
+                                       // @TODO: Indicate a parse error once it's possible. This error does not impact the logic here.
+                                       $this->generate_implied_end_tags();
+                                       $this->state->stack_of_open_elements->pop_until( 'BUTTON' );
+                               }
+
+                               $this->reconstruct_active_formatting_elements();
+                               $this->insert_html_element( $this->current_token );
+                               $this->state->frameset_ok = false;
+
+                               return true;
+
+                       /*
</ins><span class="cx" style="display: block; padding: 0 10px">                          * > A start tag whose tag name is one of: "address", "article", "aside",
</span><span class="cx" style="display: block; padding: 0 10px">                         * > "blockquote", "center", "details", "dialog", "dir", "div", "dl",
</span><span class="cx" style="display: block; padding: 0 10px">                         * > "fieldset", "figcaption", "figure", "footer", "header", "hgroup",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -535,15 +563,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        case '-BLOCKQUOTE':
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        case '-BUTTON':
</ins><span class="cx" style="display: block; padding: 0 10px">                         case '-DIV':
</span><span class="cx" style="display: block; padding: 0 10px">                        case '-FIGCAPTION':
</span><span class="cx" style="display: block; padding: 0 10px">                        case '-FIGURE':
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        // @TODO: Report parse error.
</ins><span class="cx" style="display: block; padding: 0 10px">                                         // Ignore the token.
</span><span class="cx" style="display: block; padding: 0 10px">                                        return $this->step();
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->generate_implied_end_tags();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) {
+                                       // @TODO: Record parse error: this error doesn't impact parsing.
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $this->state->stack_of_open_elements->pop_until( $tag_name );
</span><span class="cx" style="display: block; padding: 0 10px">                                return true;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorBreadcrumbsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php 2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php   2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -39,6 +39,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'A',
</span><span class="cx" style="display: block; padding: 0 10px">                        'B',
</span><span class="cx" style="display: block; padding: 0 10px">                        'BIG',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'BUTTON',
</ins><span class="cx" style="display: block; padding: 0 10px">                         'CODE',
</span><span class="cx" style="display: block; padding: 0 10px">                        'DIV',
</span><span class="cx" style="display: block; padding: 0 10px">                        'EM',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -111,7 +112,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'BLINK', // Deprecated
</span><span class="cx" style="display: block; padding: 0 10px">                        'BODY',
</span><span class="cx" style="display: block; padding: 0 10px">                        'BR',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'BUTTON',
</del><span class="cx" style="display: block; padding: 0 10px">                         'CANVAS',
</span><span class="cx" style="display: block; padding: 0 10px">                        'CAPTION',
</span><span class="cx" style="display: block; padding: 0 10px">                        'CENTER', // Neutralized
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorSemanticRulesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php       2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php 2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -16,6 +16,127 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * RULES FOR "IN BODY" MODE
</span><span class="cx" style="display: block; padding: 0 10px">         *******************************************************************/
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Verifies that when encountering an end tag for which there is no corresponding
+        * element in scope, that it skips the tag entirely.
+        *
+        * @ticket 58961
+        *
+        * @since 6.4.0
+        *
+        * @throws Exception
+        */
+       public function test_in_body_skips_unexpected_button_closer() {
+               $p = WP_HTML_Processor::createFragment( '<div>Test</button></div>' );
+
+               $p->step();
+               $this->assertEquals( 'DIV', $p->get_tag(), 'Did not stop at initial DIV tag.' );
+               $this->assertFalse( $p->is_tag_closer(), 'Did not find that initial DIV tag is an opener.' );
+
+               /*
+                * When encountering the BUTTON closing tag, there is no BUTTON in the stack of open elements.
+                * It should be ignored as there's no BUTTON to close.
+                */
+               $this->assertTrue( $p->step(), 'Found no further tags when it should have found the closing DIV' );
+               $this->assertEquals( 'DIV', $p->get_tag(), "Did not skip unexpected BUTTON; stopped at {$p->get_tag()}." );
+               $this->assertTrue( $p->is_tag_closer(), 'Did not find that the terminal DIV tag is a closer.' );
+       }
+
+       /**
+        * Verifies insertion of a BUTTON element when no existing BUTTON is already in scope.
+        *
+        * @ticket 58961
+        *
+        * @since 6.4.0
+        *
+        * @throws WP_HTML_Unsupported_Exception
+        */
+       public function test_in_body_button_with_no_button_in_scope() {
+               $p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>here</button>!</p></div><button two>not here</button>' );
+
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
+               $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
+
+               /*
+                * There's nothing special about this HTML construction, but it's important to verify that
+                * the HTML Processor can find a BUTTON under normal and normative scenarios, not just the
+                * malformed and unexpected ones.
+                */
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
+               $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
+       }
+
+       /**
+        * Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
+        * that the open button is closed with all other elements inside of it.
+        *
+        * @ticket 58961
+        *
+        * @since 6.4.0
+        *
+        * @throws WP_HTML_Unsupported_Exception
+        */
+       public function test_in_body_button_with_button_in_scope_as_parent() {
+               $p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>almost<button two>here</button>!</p></div><button three>not here</button>' );
+
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
+               $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
+
+               /*
+                * A naive parser might skip the second BUTTON because it's looking for the close of the first one,
+                * or it may place it as a child of the first one, but it implicitly closes the open BUTTON.
+                */
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
+               $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
+
+               /*
+                * This is another form of the test for the second button, but from a different side. The test is
+                * looking for proper handling of the open and close sequence for the BUTTON tags.
+                */
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
+               $this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
+       }
+
+       /**
+        * Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
+        * that the open button is closed with all other elements inside of it, even if the
+        * BUTTON in scope is not a direct parent of the new BUTTON element.
+        *
+        * @ticket 58961
+        *
+        * @since 6.4.0
+        *
+        * @throws WP_HTML_Unsupported_Exception
+        */
+       public function test_in_body_button_with_button_in_scope_as_ancestor() {
+               $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>' );
+
+               // This button finds itself normally nesting inside the DIV.
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
+               $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
+
+               /*
+                * Because the second button appears while a BUTTON is in scope, it generates implied end tags and closes
+                * the BUTTON, P, and SPAN elements. It looks like the BUTTON is inside the SPAN, but it's another case
+                * of an unexpected closing SPAN tag because the SPAN was closed by the second BUTTON. This element finds
+                * itself a child of the most-recent open element above the most-recent BUTTON, or the DIV.
+                */
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
+               $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
+
+               // The third button is back to normal, because everything has been implicitly or explicitly closed by now.
+               $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
+               $this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
+               $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /*
</span><span class="cx" style="display: block; padding: 0 10px">         * Verifies that when "in body" and encountering "any other end tag"
</span><span class="cx" style="display: block; padding: 0 10px">         * that the HTML processor ignores the end tag if there's a special
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -57,7 +178,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( 'CODE', $p->get_tag(), "Expected to start test on CODE element but found {$p->get_tag()} instead." );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertTrue( $p->step(), 'Failed to advance past CODE tag to expected SPAN closer.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlSupportRequiredOpenElementsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php  2023-08-10 01:04:35 UTC (rev 56379)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php    2023-08-10 08:35:55 UTC (rev 56380)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -176,8 +176,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'DESC' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'TITLE' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               $this->ensure_support_is_added_everywhere( 'BUTTON' );
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -218,9 +216,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'DESC' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'TITLE' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               // This element is specific to BUTTON scope.
-               $this->ensure_support_is_added_everywhere( 'BUTTON' );
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -261,8 +256,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'FOREIGNOBJECT' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'DESC' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->ensure_support_is_added_everywhere( 'TITLE' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               $this->ensure_support_is_added_everywhere( 'BUTTON' );
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre>
</div>
</div>

</body>
</html>