<!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>[55706] trunk: HTML API: Accumulate shift for internal parsing pointer.</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/55706">55706</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/55706","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-05-03 11:29:42 +0000 (Wed, 03 May 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: Accumulate shift for internal parsing pointer.

A bug was discovered where where the parser wasn't returning to the
start of the affected tag after making some updates.

In few words, the Tag Processor has not been treating its own internal
pointer `bytes_already_parsed` the same way it treats its bookmarks.
That is, when updates are applied to the input document and then
`get_updated_html()` is called, the internal pointer transfers to
the newly-updated content as if no updates had been applied since
the previous call to `get_updated_html()`.

In this patch we're creating a new "shift accumulator" to account for
all of the updates that accrue before calling `get_updated_html()`.
This accumulated shift will be applied when swapping the input document
with the output buffer, which should result in the pointer pointing to
the same logical spot in the document it did before the udpate.

In effect this patch adds a single workaround for treating the
internal pointer like a bookmark, plus a temporary pointer which points
to the beginning of the current tag when calling `get_updated_html()`.
This will preserve the assumption that updating a document doesn't
move that pointer, or shift which tag is currently matched.

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

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmltagprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlTagProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmltagprocessorphp"></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-tag-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-tag-processor.php    2023-05-03 10:01:52 UTC (rev 55705)
+++ trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php      2023-05-03 11:29:42 UTC (rev 55706)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -318,23 +318,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private $stop_on_tag_closers;
</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">-         * Holds updated HTML as updates are applied.
-        *
-        * Updates and unmodified portions of the input document are
-        * appended to this value as they are applied. It will hold
-        * a copy of the updated document up until the point of the
-        * latest applied update. The fully-updated HTML document
-        * will comprise this value plus the part of the input document
-        * which follows that latest update.
-        *
-        * @see $bytes_already_copied
-        *
-        * @since 6.2.0
-        * @var string
-        */
-       private $output_buffer = '';
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * How many bytes from the original HTML document have been read and parsed.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * This value points to the latest byte offset in the input document which
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -347,23 +330,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private $bytes_already_parsed = 0;
</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">-         * How many bytes from the input HTML document have already been
-        * copied into the output buffer.
-        *
-        * Lexical updates are enqueued and processed in batches. Prior
-        * to any given update in the input document, there might exist
-        * a span of HTML unaffected by any changes. This span ought to
-        * be copied verbatim into the output buffer before applying the
-        * following update. This value will point to the starting byte
-        * offset in the input document where that unaffected span of
-        * HTML starts.
-        *
-        * @since 6.2.0
-        * @var int
-        */
-       private $bytes_already_copied = 0;
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Byte offset in input document where current tag name starts.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Example:
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1303,8 +1269,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return void
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        private function after_tag() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->class_name_updates_to_attributes_updates();
-               $this->apply_attributes_updates();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->get_updated_html();
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->tag_name_starts_at = null;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->tag_name_length    = null;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->tag_ends_at        = null;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1460,15 +1425,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Applies attribute updates to HTML document.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.2.1 Accumulates shift for internal cursor and passed pointer.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return void
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $shift_this_point Accumulate and return shift for this position.
+        * @return int How many bytes the given pointer moved in response to the updates.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private function apply_attributes_updates() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private function apply_attributes_updates( $shift_this_point = 0 ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! count( $this->lexical_updates ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return 0;
</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">+                $accumulated_shift_for_given_point = 0;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                 * Attribute updates can be enqueued in any order but updates
</span><span class="cx" style="display: block; padding: 0 10px">                 * to the document must occur in lexical order; that is, each
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1481,12 +1450,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $bytes_already_copied = 0;
+               $output_buffer        = '';
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $this->lexical_updates as $diff ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $this->output_buffer       .= substr( $this->html, $this->bytes_already_copied, $diff->start - $this->bytes_already_copied );
-                       $this->output_buffer       .= $diff->text;
-                       $this->bytes_already_copied = $diff->end;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $shift = strlen( $diff->text ) - ( $diff->end - $diff->start );
+
+                       // Adjust the cursor position by however much an update affects it.
+                       if ( $diff->start <= $this->bytes_already_parsed ) {
+                               $this->bytes_already_parsed += $shift;
+                       }
+
+                       // Accumulate shift of the given pointer within this function call.
+                       if ( $diff->start <= $shift_this_point ) {
+                               $accumulated_shift_for_given_point += $shift;
+                       }
+
+                       $output_buffer        .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied );
+                       $output_buffer        .= $diff->text;
+                       $bytes_already_copied  = $diff->end;
</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">+                $this->html = $output_buffer . substr( $this->html, $bytes_already_copied );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                 * Adjust bookmark locations to account for how the text
</span><span class="cx" style="display: block; padding: 0 10px">                 * replacements adjust offsets in the input document.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1527,6 +1512,8 @@
</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->lexical_updates = array();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               return $accumulated_shift_for_given_point;
</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">@@ -1576,8 +1563,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Point this tag processor before the sought tag opener and consume it.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->bytes_already_copied = $this->bytes_already_parsed;
-               $this->output_buffer        = substr( $this->html, 0, $this->bytes_already_copied );
</del><span class="cx" style="display: block; padding: 0 10px">                 return $this->next_tag( array( 'tag_closers' => 'visit' ) );
</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">@@ -2122,6 +2107,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Returns the string representation of the HTML Tag Processor.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string The processed HTML.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2132,46 +2118,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * When there is nothing more to update and nothing has already been
</span><span class="cx" style="display: block; padding: 0 10px">                 * updated, return the original document and avoid a string copy.
</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 ( $requires_no_updating && 0 === $this->bytes_already_copied ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $requires_no_updating ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return $this->html;
</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">-                 * If there are no updates left to apply, but some have already
-                * been applied, then finish by copying the rest of the input
-                * to the end of the updated document and return.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * Keep track of the position right before the current tag. This will
+                * be necessary for reparsing the current tag after updating the HTML.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $requires_no_updating && $this->bytes_already_copied > 0 ) {
-                       $this->html                 = $this->output_buffer . substr( $this->html, $this->bytes_already_copied );
-                       $this->bytes_already_copied = strlen( $this->output_buffer );
-                       return $this->output_buffer . substr( $this->html, $this->bytes_already_copied );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $before_current_tag = $this->tag_name_starts_at - 1;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Apply the updates, rewind to before the current tag, and reparse the attributes.
-               $content_up_to_opened_tag_name = $this->output_buffer . substr(
-                       $this->html,
-                       $this->bytes_already_copied,
-                       $this->tag_name_starts_at + $this->tag_name_length - $this->bytes_already_copied
-               );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * 1. Apply the edits by flushing them to the output buffer and updating the copied byte count.
-                *
-                * Note: `apply_attributes_updates()` modifies `$this->output_buffer`.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * 1. Apply the enqueued edits and update all the pointers to reflect those changes.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                $this->class_name_updates_to_attributes_updates();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->apply_attributes_updates();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $before_current_tag += $this->apply_attributes_updates( $before_current_tag );
</ins><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">-                 * 2. Replace the original HTML with the now-updated HTML so that it's possible to
-                *    seek to a previous location and have a consistent view of the updated document.
-                */
-               $this->html                 = $this->output_buffer . substr( $this->html, $this->bytes_already_copied );
-               $this->output_buffer        = $content_up_to_opened_tag_name;
-               $this->bytes_already_copied = strlen( $this->output_buffer );
-
-               /*
-                * 3. Point this tag processor at the original tag opener and consume it
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * 2. Rewind to before the current tag and reparse to get updated attributes.
</ins><span class="cx" style="display: block; padding: 0 10px">                  *
</span><span class="cx" style="display: block; padding: 0 10px">                 * At this point the internal cursor points to the end of the tag name.
</span><span class="cx" style="display: block; padding: 0 10px">                 * Rewind before the tag name starts so that it's as if the cursor didn't
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2183,9 +2147,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 *                 ^  | back up by the length of the tag name plus the opening <
</span><span class="cx" style="display: block; padding: 0 10px">                 *                 \<-/ back up by strlen("em") + 1 ==> 3
</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->bytes_already_parsed = strlen( $content_up_to_opened_tag_name ) - $this->tag_name_length - 1;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // Store existing state so it can be restored after reparsing.
+               $previous_parsed_byte_count = $this->bytes_already_parsed;
+               $previous_query             = $this->last_query;
+
+               // Reparse attributes.
+               $this->bytes_already_parsed = $before_current_tag;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->next_tag();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Restore previous state.
+               $this->bytes_already_parsed = $previous_parsed_byte_count;
+               $this->parse_query( $previous_query );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $this->html;
</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="trunktestsphpunittestshtmlapiwpHtmlTagProcessorphp"></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/wpHtmlTagProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php 2023-05-03 10:01:52 UTC (rev 55705)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php   2023-05-03 11:29:42 UTC (rev 55706)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -572,6 +572,32 @@
</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">+         * Verifies that updates to a document before calls to `get_updated_html()` don't
+        * lead to the Tag Processor jumping to the wrong tag after the updates.
+        *
+        * @ticket 58179
+        *
+        * @covers WP_HTML_Tag_Processor::get_updated_html
+        */
+       public function test_internal_pointer_returns_to_original_spot_after_inserting_content_before_cursor() {
+               $tags = new WP_HTML_Tag_Processor( '<div>outside</div><section><div><img>inside</div></section>' );
+
+               $tags->next_tag();
+               $tags->add_class( 'foo' );
+               $tags->next_tag( 'section' );
+
+               // Return to this spot after moving ahead.
+               $tags->set_bookmark( 'here' );
+
+               // Move ahead.
+               $tags->next_tag( 'img' );
+               $tags->seek( 'here' );
+               $this->assertSame( '<div class="foo">outside</div><section><div><img>inside</div></section>', $tags->get_updated_html() );
+               $this->assertSame( 'SECTION', $tags->get_tag() );
+               $this->assertFalse( $tags->is_tag_closer() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 56299
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers WP_HTML_Tag_Processor::set_attribute
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1522,7 +1548,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> HTML;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $p = new WP_HTML_Tag_Processor( $input );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertTrue( $p->next_tag( 'div' ), 'Querying an existing tag did not return true' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertTrue( $p->next_tag( 'div' ), 'Did not find first DIV tag in input.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $p->set_attribute( 'data-details', '{ "key": "value" }' );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->add_class( 'is-processed' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1532,7 +1558,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        'class_name' => 'BtnGroup',
</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">-                        'Querying an existing tag did not return true'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'Did not find the first BtnGroup DIV tag'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->remove_class( 'BtnGroup' );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->add_class( 'button-group' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1544,7 +1570,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        'class_name' => 'BtnGroup',
</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">-                        'Querying an existing tag did not return true'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'Did not find the second BtnGroup DIV tag'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->remove_class( 'BtnGroup' );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->add_class( 'button-group' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1557,10 +1583,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        'match_offset' => 3,
</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">-                        'Querying an existing tag did not return true'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'Did not find third BUTTON tag with "btn" CSS class'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">                $p->remove_attribute( 'class' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertFalse( $p->next_tag( 'non-existent' ), 'Querying a non-existing tag did not return false' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertFalse( $p->next_tag( 'non-existent' ), "Found a {$p->get_tag()} tag when none should have been found." );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $p->set_attribute( 'class', 'test' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( $expected_output, $p->get_updated_html(), 'Calling get_updated_html after updating the attributes did not return the expected HTML' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre>
</div>
</div>

</body>
</html>