diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php
index cb913853f0ee9..210492ab9af08 100644
--- a/src/wp-includes/html-api/class-wp-html-open-elements.php
+++ b/src/wp-includes/html-api/class-wp-html-open-elements.php
@@ -520,11 +520,6 @@ public function pop(): bool {
return false;
}
- if ( 'context-node' === $item->bookmark_name ) {
- $this->stack[] = $item;
- return false;
- }
-
$this->after_element_pop( $item );
return true;
}
@@ -585,10 +580,6 @@ public function push( WP_HTML_Token $stack_item ): void {
* @return bool Whether the node was found and removed from the stack of open elements.
*/
public function remove_node( WP_HTML_Token $token ): bool {
- if ( 'context-node' === $token->bookmark_name ) {
- return false;
- }
-
foreach ( $this->walk_up() as $position_from_end => $item ) {
if ( $token->bookmark_name !== $item->bookmark_name ) {
continue;
diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php
index 9efcdd90aa15c..ce6500381a645 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -5409,11 +5409,14 @@ public function seek( $bookmark_name ): bool {
*/
if ( 'backward' === $direction ) {
/*
- * Instead of clearing the parser state and starting fresh, calling the stack methods
- * maintains the proper flags in the parser.
+ * When moving backward, stateful stacks should be cleared.
*/
foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
- if ( 'context-node' === $item->bookmark_name ) {
+ /*
+ * Fragment parsers always start with an HTML root node at the top of the stack.
+ * Do not remove it.
+ */
+ if ( 'root-node' === $item->bookmark_name ) {
break;
}
@@ -5421,39 +5424,68 @@ public function seek( $bookmark_name ): bool {
}
foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
- if ( 'context-node' === $item->bookmark_name ) {
- break;
- }
-
$this->state->active_formatting_elements->remove_node( $item );
}
- 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;
+ /*
+ * **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();
+
+ /*
+ * 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();
- if ( isset( $this->context_node ) ) {
- $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 );
+ $this->bookmarks['initial'] = new WP_HTML_Span( 0, 0 );
+ parent::seek( 'initial' );
+ unset( $this->bookmarks['initial'] );
} else {
- $this->breadcrumbs = array();
- }
- }
+ $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;
+ }
- // 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;
+ $this->reset_insertion_mode_appropriately();
+ $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 );
+ parent::seek( $this->context_node->bookmark_name );
+ }
}
- while ( $this->next_token() ) {
+ /*
+ * 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;
+ }
if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
- while ( isset( $this->current_element ) && WP_HTML_Stack_Event::POP === $this->current_element->operation ) {
- $this->current_element = array_shift( $this->element_queue );
- }
return true;
}
- }
+ } while ( $this->next_token() );
return false;
}
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php
new file mode 100644
index 0000000000000..eafb03c0cbaec
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php
@@ -0,0 +1,143 @@
+' );
+ $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( '' );
+ $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( '' );
+ $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( '