<!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>[58304] trunk: HTML API: Report real and virtual nodes in the HTML Processor.</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/58304">58304</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/58304","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-06-03 19:45:57 +0000 (Mon, 03 Jun 2024)</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: Report real and virtual nodes in the HTML Processor.

HTML is a kind of short-hand for a DOM structure. This means that there are
many cases in HTML where an element's opening tag or closing tag is missing (or
both). This is because many of the parsing rules imply creating elements in the
DOM which may not exist in the text of the HTML.

The HTML Processor, being the higher-level counterpart to the Tag Processor, is
already aware of these nodes, but since it's inception has not paused on them
when scanning through a document. Instead, these are visible when pausing on a
child of such an element, but otherwise not seen.

In this patch the HTML Processor starts exposing those implicitly-created nodes,
including opening tags, and closing tags, that aren't foudn in the text content
of the HTML input document.

Previously, the sequence of matched tokens when scanning with 
`WP_HTML_Processor::next_token()` would depend on how the HTML document was written,
but with this patch, all semantically equal HTML documents will parse and scan in
the same exact manner, presenting an idealized or "perfect" view of the document
the same way as would occur when traversing a DOM in a browser.

Developed in https://github.com/WordPress/wordpress-develop/pull/6348
Discussed in https://core.trac.wordpress.org/ticket/61348

Props audrasjb, dmsnell, gziolo, jonsurrell.
Fixes <a href="https://core.trac.wordpress.org/ticket/61348">#61348</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="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.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>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlstackeventphp">trunk/src/wp-includes/html-api/class-wp-html-stack-event.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    2024-06-03 18:30:02 UTC (rev 58303)
+++ trunk/src/wp-includes/html-api/class-wp-html-open-elements.php      2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -52,6 +52,56 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private $has_p_in_button_scope = false;
</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">+         * A function that will be called when an item is popped off the stack of open elements.
+        *
+        * The function will be called with the popped item as its argument.
+        *
+        * @since 6.6.0
+        *
+        * @var Closure
+        */
+       private $pop_handler = null;
+
+       /**
+        * A function that will be called when an item is pushed onto the stack of open elements.
+        *
+        * The function will be called with the pushed item as its argument.
+        *
+        * @since 6.6.0
+        *
+        * @var Closure
+        */
+       private $push_handler = null;
+
+       /**
+        * Sets a pop handler that will be called when an item is popped off the stack of
+        * open elements.
+        *
+        * The function will be called with the pushed item as its argument.
+        *
+        * @since 6.6.0
+        *
+        * @param Closure $handler The handler function.
+        */
+       public function set_pop_handler( Closure $handler ) {
+               $this->pop_handler = $handler;
+       }
+
+       /**
+        * Sets a push handler that will be called when an item is pushed onto the stack of
+        * open elements.
+        *
+        * The function will be called with the pushed item as its argument.
+        *
+        * @since 6.6.0
+        *
+        * @param Closure $handler The handler function.
+        */
+       public function set_push_handler( Closure $handler ) {
+               $this->push_handler = $handler;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Reports if a specific node is in the stack of open elements.
</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 class="lines" style="display: block; padding: 0 10px; color: #888">@@ -429,6 +479,10 @@
</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="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               if ( null !== $this->push_handler ) {
+                       ( $this->push_handler )( $item );
+               }
</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">@@ -458,5 +512,9 @@
</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 class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               if ( null !== $this->pop_handler ) {
+                       ( $this->pop_handler )( $item );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</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        2024-06-03 18:30:02 UTC (rev 58303)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -201,6 +201,52 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        private $release_internal_bookmark_on_destruct = null;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Stores stack events which arise during parsing of the
+        * HTML document, which will then supply the "match" events.
+        *
+        * @since 6.6.0
+        *
+        * @var WP_HTML_Stack_Event[]
+        */
+       private $element_queue = array();
+
+       /**
+        * Current stack event, if set, representing a matched token.
+        *
+        * Because the parser may internally point to a place further along in a document
+        * than the nodes which have already been processed (some "virtual" nodes may have
+        * appeared while scanning the HTML document), this will point at the "current" node
+        * being processed. It comes from the front of the element queue.
+        *
+        * @since 6.6.0
+        *
+        * @var ?WP_HTML_Stack_Event
+        */
+       private $current_element = null;
+
+       /**
+        * Context node if created as a fragment parser.
+        *
+        * @var ?WP_HTML_Token
+        */
+       private $context_node = null;
+
+       /**
+        * Whether the parser has yet processed the context node,
+        * if created as a fragment parser.
+        *
+        * The context node will be initially pushed onto the stack of open elements,
+        * but when created as a fragment parser, this context element (and the implicit
+        * HTML document node above it) should not be exposed as a matched token or node.
+        *
+        * This boolean indicates whether the processor should skip over the current
+        * node in its initial search for the first node created from the input HTML.
+        *
+        * @var bool
+        */
+       private $has_seen_context_node = false;
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /*
</span><span class="cx" style="display: block; padding: 0 10px">         * Public Interface Functions
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -257,14 +303,15 @@
</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"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $processor->state->stack_of_open_elements->push(
-                       new WP_HTML_Token(
-                               'context-node',
-                               $processor->state->context_node[0],
-                               false
-                       )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $context_node = new WP_HTML_Token(
+                       'context-node',
+                       $processor->state->context_node[0],
+                       false
</ins><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">+                $processor->state->stack_of_open_elements->push( $context_node );
+               $processor->context_node = $context_node;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $processor;
</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">@@ -299,6 +346,18 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->state = new WP_HTML_Processor_State();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->state->stack_of_open_elements->set_push_handler(
+                       function ( WP_HTML_Token $token ) {
+                               $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH );
+                       }
+               );
+
+               $this->state->stack_of_open_elements->set_pop_handler(
+                       function ( WP_HTML_Token $token ) {
+                               $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP );
+                       }
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                 * Create this wrapper so that it's possible to pass
</span><span class="cx" style="display: block; padding: 0 10px">                 * a private method into WP_HTML_Token classes without
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -342,6 +401,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @todo Support matching the class name and tag name.
</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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.6.0 Visits all tokens, including virtual ones.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -349,6 +409,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *     Optional. Which tag name to find, having which class, etc. Default is to find any tag.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         *     @type string|null $tag_name     Which tag to find, or `null` for "any tag."
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *     @type string      $tag_closers  'visit' to pause at tag closers, 'skip' or unset to only visit openers.
</ins><span class="cx" style="display: block; padding: 0 10px">          *     @type int|null    $match_offset Find the Nth tag matching all search criteria.
</span><span class="cx" style="display: block; padding: 0 10px">         *                                     1 for "first" tag, 3 for "third," etc.
</span><span class="cx" style="display: block; padding: 0 10px">         *                                     Defaults to first tag.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -359,13 +420,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool Whether a tag was matched.
</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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $visit_closers = isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'];
+
</ins><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">-                        while ( $this->step() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 while ( $this->next_token() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( '#tag' !== $this->get_token_type() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        continue;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! $this->is_tag_closer() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! $this::is_tag_closer() || $visit_closers ) {
</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">@@ -391,7 +454,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        : null;
</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">-                        while ( $this->step() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 while ( $this->next_token() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( '#tag' !== $this->get_token_type() ) {
</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">@@ -400,7 +463,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        continue;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! $this->is_tag_closer() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! parent::is_tag_closer() || $visit_closers ) {
</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">@@ -408,20 +471,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'] ) {
-                       _doing_it_wrong(
-                               __METHOD__,
-                               __( 'Cannot visit tag closers in HTML Processor.' ),
-                               '6.4.0'
-                       );
-                       return false;
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $breadcrumbs  = $query['breadcrumbs'];
</span><span class="cx" style="display: block; padding: 0 10px">                $match_offset = isset( $query['match_offset'] ) ? (int) $query['match_offset'] : 1;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                while ( $match_offset > 0 && $this->step() ) {
-                       if ( '#tag' !== $this->get_token_type() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         while ( $match_offset > 0 && $this->next_token() ) {
+                       if ( '#tag' !== $this->get_token_type() || $this->is_tag_closer() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 continue;
</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">@@ -452,10 +506,74 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function next_token() {
</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">+         $this->current_element = null;
+
+               if ( isset( $this->last_error ) ) {
+                       return false;
+               }
+
+               if ( 0 === count( $this->element_queue ) && ! $this->step() ) {
+                       while ( $this->state->stack_of_open_elements->pop() ) {
+                               continue;
+                       }
+               }
+
+               $this->current_element = array_shift( $this->element_queue );
+               while ( isset( $this->context_node ) && ! $this->has_seen_context_node ) {
+                       if ( isset( $this->current_element ) ) {
+                               if ( $this->context_node === $this->current_element->token && WP_HTML_Stack_Event::PUSH === $this->current_element->operation ) {
+                                       $this->has_seen_context_node = true;
+                                       return $this->next_token();
+                               }
+                       }
+                       $this->current_element = array_shift( $this->element_queue );
+               }
+
+               if ( ! isset( $this->current_element ) ) {
+                       return $this->next_token();
+               }
+
+               if ( isset( $this->context_node ) && WP_HTML_Stack_Event::POP === $this->current_element->operation && $this->context_node === $this->current_element->token ) {
+                       $this->element_queue   = array();
+                       $this->current_element = null;
+                       return false;
+               }
+
+               // Avoid sending close events for elements which don't expect a closing.
+               if (
+                       WP_HTML_Stack_Event::POP === $this->current_element->operation &&
+                       ! static::expects_closer( $this->current_element->token->node_name )
+               ) {
+                       return $this->next_token();
+               }
+
+               return true;
</ins><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">+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Indicates if the current tag token is a tag closer.
+        *
+        * Example:
+        *
+        *     $p = WP_HTML_Processor::create_fragment( '<div></div>' );
+        *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
+        *     $p->is_tag_closer() === false;
+        *
+        *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
+        *     $p->is_tag_closer() === true;
+        *
+        * @since 6.6.0 Subclassed for HTML Processor.
+        *
+        * @return bool Whether the current tag is a tag closer.
+        */
+       public function is_tag_closer() {
+               return isset( $this->current_element )
+                       ? ( WP_HTML_Stack_Event::POP === $this->current_element->operation )
+                       : parent::is_tag_closer();
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Indicates if the currently-matched tag matches the given breadcrumbs.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * A "*" represents a single tag wildcard, where any tag matches, but not no tags.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -525,11 +643,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *       this returns false for self-closing elements in the
</span><span class="cx" style="display: block; padding: 0 10px">         *       SVG and MathML namespace.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param  ?WP_HTML_Token $node Node to examine instead of current node, if provided.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return bool Whether to expect a closer for the currently-matched node,
</span><span class="cx" style="display: block; padding: 0 10px">         *              or `null` if not matched on any token.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function expects_closer() {
-               $token_name = $this->get_token_name();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function expects_closer( $node = null ) {
+               $token_name = $node->node_name ?? $this->get_token_name();
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! isset( $token_name ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return null;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -581,16 +700,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         *        is provided in the opening tag, otherwise it expects a tag closer.
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        $top_node = $this->state->stack_of_open_elements->current_node();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if (
-                               $top_node && (
-                                       // Void elements.
-                                       self::is_void( $top_node->node_name ) ||
-                                       // Comments, text nodes, and other atomic tokens.
-                                       '#' === $top_node->node_name[0] ||
-                                       // Doctype declarations.
-                                       'html' === $top_node->node_name
-                               )
-                       ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( isset( $top_node ) && ! static::expects_closer( $top_node ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $this->state->stack_of_open_elements->pop();
</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">@@ -650,6 +760,8 @@
</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 class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @todo make aware of queue of elements, because stack operations have already been done by now.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return string[]|null Array of tag names representing path to matched node, if matched, otherwise NULL.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_breadcrumbs() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -708,7 +820,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private function step_in_body() {
</span><span class="cx" style="display: block; padding: 0 10px">                $token_name = $this->get_token_name();
</span><span class="cx" style="display: block; padding: 0 10px">                $token_type = $this->get_token_type();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
</ins><span class="cx" style="display: block; padding: 0 10px">                 $op         = "{$op_sigil}{$token_name}";
</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="lines" style="display: block; padding: 0 10px; color: #888">@@ -1231,7 +1343,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                throw new WP_HTML_Unsupported_Exception( "Cannot process {$token_name} element." );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! $this->is_tag_closer() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! parent::is_tag_closer() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         /*
</span><span class="cx" style="display: block; padding: 0 10px">                         * > Any other start tag
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1327,6 +1439,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return 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">+                if ( isset( $this->current_element ) ) {
+                       return $this->current_element->token->node_name;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $tag_name = parent::get_tag();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                switch ( $tag_name ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1343,6 +1459,189 @@
</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">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Returns the node name represented by the token.
+        *
+        * This matches the DOM API value `nodeName`. Some values
+        * are static, such as `#text` for a text node, while others
+        * are dynamically generated from the token itself.
+        *
+        * Dynamic names:
+        *  - Uppercase tag name for tag matches.
+        *  - `html` for DOCTYPE declarations.
+        *
+        * Note that if the Tag Processor is not matched on a token
+        * then this function will return `null`, either because it
+        * hasn't yet found a token or because it reached the end
+        * of the document without matching a token.
+        *
+        * @since 6.6.0 Subclassed for the HTML Processor.
+        *
+        * @return string|null Name of the matched token.
+        */
+       public function get_token_name() {
+               if ( isset( $this->current_element ) ) {
+                       return $this->current_element->token->node_name;
+               }
+
+               return parent::get_token_name();
+       }
+
+       /**
+        * Indicates the kind of matched token, if any.
+        *
+        * This differs from `get_token_name()` in that it always
+        * returns a static string indicating the type, whereas
+        * `get_token_name()` may return values derived from the
+        * token itself, such as a tag name or processing
+        * instruction tag.
+        *
+        * Possible values:
+        *  - `#tag` when matched on a tag.
+        *  - `#text` when matched on a text node.
+        *  - `#cdata-section` when matched on a CDATA node.
+        *  - `#comment` when matched on a comment.
+        *  - `#doctype` when matched on a DOCTYPE declaration.
+        *  - `#presumptuous-tag` when matched on an empty tag closer.
+        *  - `#funky-comment` when matched on a funky comment.
+        *
+        * @since 6.6.0 Subclassed for the HTML Processor.
+        *
+        * @return string|null What kind of token is matched, or null.
+        */
+       public function get_token_type() {
+               if ( isset( $this->current_element ) ) {
+                       $node_name = $this->current_element->token->node_name;
+                       if ( ctype_upper( $node_name[0] ) ) {
+                               return '#tag';
+                       }
+
+                       if ( 'html' === $node_name ) {
+                               return '#doctype';
+                       }
+
+                       return $node_name;
+               }
+
+               return parent::get_token_type();
+       }
+
+       /**
+        * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
+        *
+        * Example:
+        *
+        *     $p = WP_HTML_Processor::create_fragment( '<div enabled class="test" data-test-id="14">Test</div>' );
+        *     $p->next_token() === true;
+        *     $p->get_attribute( 'data-test-id' ) === '14';
+        *     $p->get_attribute( 'enabled' ) === true;
+        *     $p->get_attribute( 'aria-label' ) === null;
+        *
+        *     $p->next_tag() === false;
+        *     $p->get_attribute( 'class' ) === null;
+        *
+        * @since 6.6.0 Subclassed for HTML Processor.
+        *
+        * @param string $name Name of attribute whose value is requested.
+        * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`.
+        */
+       public function get_attribute( $name ) {
+               if ( isset( $this->current_element ) ) {
+                       // Closing tokens cannot contain attributes.
+                       if ( WP_HTML_Stack_Event::POP === $this->current_element->operation ) {
+                               return null;
+                       }
+
+                       $node_name = $this->current_element->token->node_name;
+
+                       // Only tags can contain attributes.
+                       if ( 'A' > $node_name[0] || 'Z' < $node_name[0] ) {
+                               return null;
+                       }
+
+                       if ( $this->current_element->token->bookmark_name === (string) $this->bookmark_counter ) {
+                               return parent::get_attribute( $name );
+                       }
+               }
+
+               return null;
+       }
+
+       /**
+        * Gets lowercase names of all attributes matching a given prefix in the current tag.
+        *
+        * Note that matching is case-insensitive. This is in accordance with the spec:
+        *
+        * > There must never be two or more attributes on
+        * > the same start tag whose names are an ASCII
+        * > case-insensitive match for each other.
+        *     - HTML 5 spec
+        *
+        * Example:
+        *
+        *     $p = new WP_HTML_Tag_Processor( '<div data-ENABLED class="test" DATA-test-id="14">Test</div>' );
+        *     $p->next_tag( array( 'class_name' => 'test' ) ) === true;
+        *     $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' );
+        *
+        *     $p->next_tag() === false;
+        *     $p->get_attribute_names_with_prefix( 'data-' ) === null;
+        *
+        * @since 6.6.0 Subclassed for the HTML Processor.
+        *
+        * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
+        *
+        * @param string $prefix Prefix of requested attribute names.
+        * @return array|null List of attribute names, or `null` when no tag opener is matched.
+        */
+       public function get_attribute_names_with_prefix( $prefix ) {
+               if ( isset( $this->current_element ) ) {
+                       if ( WP_HTML_Stack_Event::POP === $this->current_element->operation ) {
+                               return null;
+                       }
+
+                       $mark = $this->bookmarks[ $this->current_element->token->bookmark_name ];
+                       if ( 0 === $mark->length ) {
+                               return null;
+                       }
+               }
+
+               return parent::get_attribute_names_with_prefix( $prefix );
+       }
+
+       /**
+        * Returns the modifiable text for a matched token, or an empty string.
+        *
+        * Modifiable text is text content that may be read and changed without
+        * changing the HTML structure of the document around it. This includes
+        * the contents of `#text` nodes in the HTML as well as the inner
+        * contents of HTML comments, Processing Instructions, and others, even
+        * though these nodes aren't part of a parsed DOM tree. They also contain
+        * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any
+        * other section in an HTML document which cannot contain HTML markup (DATA).
+        *
+        * If a token has no modifiable text then an empty string is returned to
+        * avoid needless crashing or type errors. An empty string does not mean
+        * that a token has modifiable text, and a token with modifiable text may
+        * have an empty string (e.g. a comment with no contents).
+        *
+        * @since 6.6.0 Subclassed for the HTML Processor.
+        *
+        * @return string
+        */
+       public function get_modifiable_text() {
+               if ( isset( $this->current_element ) ) {
+                       if ( WP_HTML_Stack_Event::POP === $this->current_element->operation ) {
+                               return '';
+                       }
+
+                       $mark = $this->bookmarks[ $this->current_element->token->bookmark_name ];
+                       if ( 0 === $mark->length ) {
+                               return '';
+                       }
+               }
+               return parent::get_modifiable_text();
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Removes a bookmark that is no longer needed.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Releasing a bookmark frees up the small
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1383,6 +1682,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ? $this->bookmarks[ $this->state->current_token->bookmark_name ]->start
</span><span class="cx" style="display: block; padding: 0 10px">                        : 0;
</span><span class="cx" style="display: block; padding: 0 10px">                $bookmark_starts_at   = $this->bookmarks[ $actual_bookmark_name ]->start;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $bookmark_length      = $this->bookmarks[ $actual_bookmark_name ]->length;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $direction            = $bookmark_starts_at > $processor_started_at ? 'forward' : 'backward';
</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">@@ -1438,6 +1738,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        parent::seek( 'context-node' );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->state->frameset_ok    = true;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $this->element_queue         = array();
+                       $this->current_element       = null;
</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">                // When moving forwards, reparse the document until reaching the same location as the original bookmark.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1445,8 +1747,11 @@
</span><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><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 ( $this->next_token() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                while ( isset( $this->current_element ) && WP_HTML_Stack_Event::POP === $this->current_element->operation ) {
+                                       $this->current_element = array_shift( $this->element_queue );
+                               }
</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></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmlstackeventphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/html-api/class-wp-html-stack-event.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-stack-event.php                              (rev 0)
+++ trunk/src/wp-includes/html-api/class-wp-html-stack-event.php        2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,69 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * HTML API: WP_HTML_Stack_Event class
+ *
+ * @package WordPress
+ * @subpackage HTML-API
+ * @since 6.6.0
+ */
+
+/**
+ * Core class used by the HTML Processor as a record for stack operations.
+ *
+ * This class is for internal usage of the WP_HTML_Processor class.
+ *
+ * @access private
+ * @since 6.6.0
+ *
+ * @see WP_HTML_Processor
+ */
+class WP_HTML_Stack_Event {
+       /**
+        * Refers to popping an element off of the stack of open elements.
+        *
+        * @since 6.6.0
+        */
+       const POP = 'pop';
+
+       /**
+        * Refers to pushing an element onto the stack of open elements.
+        *
+        * @since 6.6.0
+        */
+       const PUSH = 'push';
+
+       /**
+        * References the token associated with the stack push event,
+        * even if this is a pop event for that element.
+        *
+        * @since 6.6.0
+        *
+        * @var WP_HTML_Token
+        */
+       public $token;
+
+       /**
+        * Indicates which kind of stack operation this event represents.
+        *
+        * May be one of the class constants.
+        *
+        * @since 6.6.0
+        *
+        * @see self::POP
+        * @see self::PUSH
+        *
+        * @var string
+        */
+       public $operation;
+
+       /**
+        * Constructor function.
+        *
+        * @param WP_HTML_Token $token     Token associated with stack event, always an opening token.
+        * @param string        $operation One of self::PUSH or self::POP.
+        */
+       public function __construct( $token, $operation ) {
+               $this->token     = $token;
+               $this->operation = $operation;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/html-api/class-wp-html-stack-event.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-06-03 18:30:02 UTC (rev 58303)
+++ trunk/src/wp-settings.php   2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -259,6 +259,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-active-formatting-elements.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-open-elements.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-token.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/html-api/class-wp-html-stack-event.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-processor-state.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-processor.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-http.php';
</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 2024-06-03 18:30:02 UTC (rev 58303)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php   2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -231,12 +231,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_fails_when_encountering_unsupported_markup( $html, $description ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $processor = WP_HTML_Processor::create_fragment( $html );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                while ( $processor->step() && null === $processor->get_attribute( 'supported' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         while ( $processor->next_token() && null === $processor->get_attribute( 'supported' ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         continue;
</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">+                $this->assertNull(
+                       $processor->get_last_error(),
+                       'Bailed on unsupported input before finding supported checkpoint: check test code.'
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( $processor->get_attribute( 'supported' ), 'Did not find required supported element.' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertFalse( $processor->step(), "Didn't properly reject unsupported markup: {$description}" );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $processor->next_token();
+               $this->assertNotNull( $processor->get_last_error(), "Didn't properly reject unsupported markup: {$description}" );
</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">@@ -247,7 +253,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public static function data_unsupported_markup() {
</span><span class="cx" style="display: block; padding: 0 10px">                return array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'A with formatting following unclosed A' => array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                '<a><strong>Click <a supported><big unsupported>Here</big></a></strong></a>',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         '<a><strong>Click <span supported><a unsupported><big>Here</big></a></strong></a>',
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'Unclosed formatting requires complicated reconstruction.',
</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">@@ -325,7 +331,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'IMG after invalid DIV closer'          => array( '</div><img target>', array( 'HTML', 'BODY', 'IMG' ), 1 ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'EM inside DIV'                         => array( '<div>The weather is <em target>beautiful</em>.</div>', array( 'HTML', 'BODY', 'DIV', 'EM' ), 1 ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'EM after closed EM'                    => array( '<em></em><em target></em>', array( 'HTML', 'BODY', 'EM' ), 2 ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'EM after closed EMs'                   => array( '<em></em><em><em></em></em><em></em><em></em><em target></em>', array( 'HTML', 'BODY', 'EM' ), 6 ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'EM after closed EMs'                   => array( '<em></em><em><em></em></em><em></em><em></em><em target></em>', array( 'HTML', 'BODY', 'EM' ), 5 ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'EM after unclosed EM'                  => array( '<em><em target></em>', array( 'HTML', 'BODY', 'EM', 'EM' ), 1 ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'EM after unclosed EM after DIV'        => array( '<em><div><em target>', array( 'HTML', 'BODY', 'EM', 'DIV', 'EM' ), 1 ),
</span><span class="cx" style="display: block; padding: 0 10px">                        // This should work for all formatting elements, but if two work, the others probably do too.
</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       2024-06-03 18:30:02 UTC (rev 58303)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php 2024-06-03 19:45:57 UTC (rev 58304)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -387,7 +387,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( 'CODE', $processor->get_tag(), "Expected to start test on CODE element but found {$processor->get_tag()} instead." );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $processor->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( $processor->step(), '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( $processor->next_token(), 'Failed to advance past CODE tag to expected SPAN closer.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( $processor->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' ), $processor->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>
</div>

</body>
</html>