<!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>[59391] trunk: HTML API: Ensure that full processor can seek to earlier bookmarks.</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/59391">59391</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/59391","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>2024-11-12 10:30:26 +0000 (Tue, 12 Nov 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: Ensure that full processor can seek to earlier bookmarks.

When the HTML Processor seeks to an earlier place, it returns the the beginning of the document and proceeds forward until it reaches the appropriate location. This requires resetting internal state so that the processor can correctly proceed from the beginning of the document.

The seeking reset logic was not adapted to account for the full processor (i.e. when created via `WP_HTML_Processor::create_full_parser()`). This change updates the seek logic to account for the full and fragment parsers as well as other state that has been introduced in the interim and should be reset.

Props jonsurrell, dmsnell, westonruter, mi5t4n.
Fixes <a href="https://core.trac.wordpress.org/ticket/62290">#62290</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="#trunktestsphpunittestshtmlapiwpHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorbookmarkphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.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-11-12 05:44:36 UTC (rev 59390)
+++ trunk/src/wp-includes/html-api/class-wp-html-open-elements.php      2024-11-12 10:30:26 UTC (rev 59391)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -520,11 +520,6 @@
</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 ( 'context-node' === $item->bookmark_name ) {
-                       $this->stack[] = $item;
-                       return false;
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $this->after_element_pop( $item );
</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="lines" style="display: block; padding: 0 10px; color: #888">@@ -585,10 +580,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool Whether the node was found and removed from 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">        public function remove_node( WP_HTML_Token $token ): bool {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( 'context-node' === $token->bookmark_name ) {
-                       return false;
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $this->walk_up() as $position_from_end => $item ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $token->bookmark_name !== $item->bookmark_name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                continue;
</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-11-12 05:44:36 UTC (rev 59390)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2024-11-12 10:30:26 UTC (rev 59391)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5328,52 +5328,92 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * and computation time.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                if ( 'backward' === $direction ) {
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                         * Instead of clearing the parser state and starting fresh, calling the stack methods
-                        * maintains the proper flags in the parser.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                  * When moving backward, stateful stacks should be cleared.
</ins><span class="cx" style="display: block; padding: 0 10px">                          */
</span><span class="cx" style="display: block; padding: 0 10px">                        foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( 'context-node' === $item->bookmark_name ) {
-                                       break;
-                               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                                 $this->state->stack_of_open_elements->remove_node( $item );
</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">                        foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( 'context-node' === $item->bookmark_name ) {
-                                       break;
-                               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                                 $this->state->active_formatting_elements->remove_node( $item );
</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">-                        parent::seek( 'context-node' );
-                       $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
-                       $this->state->frameset_ok    = true;
-                       $this->element_queue         = array();
-                       $this->current_element       = null;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * **After** clearing stacks, more processor state can be reset.
+                        * This must be done after clearing the stack because those stacks generate events that
+                        * would appear on a subsequent call to `next_token()`.
+                        */
+                       $this->state->frameset_ok                       = true;
+                       $this->state->stack_of_template_insertion_modes = array();
+                       $this->state->head_element                      = null;
+                       $this->state->form_element                      = null;
+                       $this->state->current_token                     = null;
+                       $this->current_element                          = null;
+                       $this->element_queue                            = array();
</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 ( isset( $this->context_node ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * The absence of a context node indicates a full parse.
+                        * The presence of a context node indicates a fragment parser.
+                        */
+                       if ( null === $this->context_node ) {
+                               $this->change_parsing_namespace( 'html' );
+                               $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_INITIAL;
+                               $this->breadcrumbs           = array();
+
+                               $this->bookmarks['initial'] = new WP_HTML_Span( 0, 0 );
+                               parent::seek( 'initial' );
+                               unset( $this->bookmarks['initial'] );
+                       } else {
+
+                               /*
+                                * Push the root-node (HTML) back onto the stack of open elements.
+                                *
+                                * Fragment parsers require this extra bit of setup.
+                                * It's handled in full parsers by advancing the processor state.
+                                */
+                               $this->state->stack_of_open_elements->push(
+                                       new WP_HTML_Token(
+                                               'root-node',
+                                               'HTML',
+                                               false
+                                       )
+                               );
+
+                               $this->change_parsing_namespace(
+                                       $this->context_node->integration_node_type
+                                               ? 'html'
+                                               : $this->context_node->namespace
+                               );
+
+                               if ( 'TEMPLATE' === $this->context_node->node_name ) {
+                                       $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
+                               }
+
+                               $this->reset_insertion_mode_appropriately();
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        } else {
-                               $this->breadcrumbs = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         parent::seek( $this->context_node->bookmark_name );
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // When moving forwards, reparse the document until reaching the same location as the original bookmark.
-               if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
-                       return true;
-               }
-
-               while ( $this->next_token() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Here, the processor moves forward through the document until it matches the bookmark.
+                * do-while is used here because the processor is expected to already be stopped on
+                * a token than may match the bookmarked location.
+                */
+               do {
+                       /*
+                        * The processor will stop on virtual tokens, but bookmarks may not be set on them.
+                        * They should not be matched when seeking a bookmark, skip them.
+                        */
+                       if ( $this->is_virtual() ) {
+                               continue;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
</span><del style="background-color: #fdd; 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 );
-                               }
</del><span class="cx" style="display: block; padding: 0 10px">                                 return true;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                }
</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"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return false;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorbookmarkphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php                           (rev 0)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php     2024-11-12 10:30:26 UTC (rev 59391)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,160 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_HTML_Processor bookmark functionality.
+ *
+ * @package WordPress
+ * @subpackage HTML-API
+ */
+
+/**
+ * @group html-api
+ *
+ * @coversDefaultClass WP_HTML_Processor
+ */
+class Tests_HtmlApi_WpHtmlProcessor_Bookmark extends WP_UnitTestCase {
+       /**
+        * @dataProvider data_processor_constructors
+        *
+        * @ticket 62290
+        */
+       public function test_processor_seek_same_location( callable $factory ) {
+               $processor = $factory( '<div><span>' );
+               $this->assertTrue( $processor->next_tag( 'DIV' ) );
+               $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark.' );
+               $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed has_bookmark check.' );
+
+               // Confirm the bookmark works and processing continues normally.
+               $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark.' );
+               $this->assertSame( 'DIV', $processor->get_tag() );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs() );
+               $this->assertTrue( $processor->next_tag() );
+               $this->assertSame( 'SPAN', $processor->get_tag() );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN' ), $processor->get_breadcrumbs() );
+       }
+
+       /**
+        * @dataProvider data_processor_constructors
+        *
+        * @ticket 62290
+        */
+       public function test_processor_seek_backward( callable $factory ) {
+               $processor = $factory( '<div><span>' );
+               $this->assertTrue( $processor->next_tag( 'DIV' ) );
+               $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark.' );
+               $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed has_bookmark check.' );
+
+               // Move past the bookmark so it must scan backwards.
+               $this->assertTrue( $processor->next_tag( 'SPAN' ) );
+
+               // Confirm the bookmark works.
+               $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark.' );
+               $this->assertSame( 'DIV', $processor->get_tag() );
+       }
+
+       /**
+        * @dataProvider data_processor_constructors
+        *
+        * @ticket 62290
+        */
+       public function test_processor_seek_forward( callable $factory ) {
+               $processor = $factory( '<div one></div><span two></span><a three>' );
+               $this->assertTrue( $processor->next_tag( 'DIV' ) );
+               $this->assertTrue( $processor->set_bookmark( 'one' ), 'Failed to set bookmark "one".' );
+               $this->assertTrue( $processor->has_bookmark( 'one' ), 'Failed "one" has_bookmark check.' );
+
+               // Move past the bookmark so it must scan backwards.
+               $this->assertTrue( $processor->next_tag( 'SPAN' ) );
+               $this->assertTrue( $processor->get_attribute( 'two' ) );
+               $this->assertTrue( $processor->set_bookmark( 'two' ), 'Failed to set bookmark "two".' );
+               $this->assertTrue( $processor->has_bookmark( 'two' ), 'Failed "two" has_bookmark check.' );
+
+               // Seek back.
+               $this->assertTrue( $processor->seek( 'one' ), 'Failed to seek to bookmark "one".' );
+               $this->assertSame( 'DIV', $processor->get_tag() );
+
+               // Seek forward and continue processing.
+               $this->assertTrue( $processor->seek( 'two' ), 'Failed to seek to bookmark "two".' );
+               $this->assertSame( 'SPAN', $processor->get_tag() );
+               $this->assertTrue( $processor->get_attribute( 'two' ) );
+
+               $this->assertTrue( $processor->next_tag() );
+               $this->assertSame( 'A', $processor->get_tag() );
+               $this->assertTrue( $processor->get_attribute( 'three' ) );
+       }
+
+       /**
+        * Ensure the parsing namespace is handled when seeking from foreign content.
+        *
+        * @dataProvider data_processor_constructors
+        *
+        * @ticket 62290
+        */
+       public function test_seek_back_from_foreign_content( callable $factory ) {
+               $processor = $factory( '<custom-element /><svg><rect />' );
+               $this->assertTrue( $processor->next_tag( 'CUSTOM-ELEMENT' ) );
+               $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark "mark".' );
+               $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed "mark" has_bookmark check.' );
+
+               /*
+                * <custom-element /> has self-closing flag, but HTML elements (that are not void elements) cannot self-close,
+                * they must be closed by some means, usually a closing tag.
+                *
+                * If the div were interpreted as foreign content, it would self-close.
+                */
+               $this->assertTrue( $processor->has_self_closing_flag() );
+               $this->assertTrue( $processor->expects_closer(), 'Incorrectly interpreted HTML custom-element with self-closing flag as self-closing element.' );
+
+               // Proceed into foreign content.
+               $this->assertTrue( $processor->next_tag( 'RECT' ) );
+               $this->assertSame( 'svg', $processor->get_namespace() );
+               $this->assertTrue( $processor->has_self_closing_flag() );
+               $this->assertFalse( $processor->expects_closer() );
+               $this->assertSame( array( 'HTML', 'BODY', 'CUSTOM-ELEMENT', 'SVG', 'RECT' ), $processor->get_breadcrumbs() );
+
+               // Seek back.
+               $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark "mark".' );
+               $this->assertSame( 'CUSTOM-ELEMENT', $processor->get_tag() );
+               // If the parsing namespace were not correct here (html),
+               // then the self-closing flag would be misinterpreted.
+               $this->assertTrue( $processor->has_self_closing_flag() );
+               $this->assertTrue( $processor->expects_closer(), 'Incorrectly interpreted HTML custom-element with self-closing flag as self-closing element.' );
+
+               // Proceed into foreign content again.
+               $this->assertTrue( $processor->next_tag( 'RECT' ) );
+               $this->assertSame( 'svg', $processor->get_namespace() );
+               $this->assertTrue( $processor->has_self_closing_flag() );
+               $this->assertFalse( $processor->expects_closer() );
+
+               // The RECT should still descend from the CUSTOM-ELEMENT despite its self-closing flag.
+               $this->assertSame( array( 'HTML', 'BODY', 'CUSTOM-ELEMENT', 'SVG', 'RECT' ), $processor->get_breadcrumbs() );
+       }
+
+       /**
+        * Covers a regression where the root node may not be present on the stack of open elements.
+        *
+        * Heading elements (h1, h2, etc.) check the current node on the stack of open elements
+        * and expect it to be defined. If the root-node has been popped, pushing a new heading
+        * onto the stack will create a warning and fail the test.
+        *
+        * @ticket 62290
+        */
+       public function test_fragment_starts_with_h1() {
+               $processor = WP_HTML_Processor::create_fragment( '<h1>' );
+               $this->assertTrue( $processor->next_tag( 'H1' ) );
+               $this->assertTrue( $processor->set_bookmark( 'mark' ) );
+               $this->assertTrue( $processor->next_token() );
+               $this->assertTrue( $processor->seek( 'mark' ) );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public static function data_processor_constructors(): array {
+               return array(
+                       'Full parser'     => array( array( WP_HTML_Processor::class, 'create_full_parser' ) ),
+                       'Fragment parser' => array( array( WP_HTML_Processor::class, 'create_fragment' ) ),
+               );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorphp"></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/wpHtmlProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php    2024-11-12 05:44:36 UTC (rev 59390)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php      2024-11-12 10:30:26 UTC (rev 59391)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -133,7 +133,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Create a bookmark inside of that stack.
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( null !== $processor->get_attribute( 'two' ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $processor->set_bookmark( 'two' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $this->assertTrue( $processor->set_bookmark( 'two' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 break;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span></span></pre>
</div>
</div>

</body>
</html>