Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,14 @@ class WP_HTML_Tag_Processor {
*/
protected $bookmarks = array();

/**
* Tracks bookmarks set on text nodes whose leading linefeed is ignored.
*
* @since 6.9.0
* @var bool[]
*/
private $bookmarks_with_skipped_newline = array();

const ADD_CLASS = true;
const REMOVE_CLASS = false;
const SKIP_CLASS = null;
Expand Down Expand Up @@ -1357,6 +1365,11 @@ public function set_bookmark( $name ): bool {
}

$this->bookmarks[ $name ] = new WP_HTML_Span( $this->token_starts_at, $this->token_length );
if ( self::STATE_TEXT_NODE === $this->parser_state && $this->skip_newline_at === $this->token_starts_at ) {
$this->bookmarks_with_skipped_newline[ $name ] = true;
} else {
unset( $this->bookmarks_with_skipped_newline[ $name ] );
}

return true;
}
Expand All @@ -1377,6 +1390,7 @@ public function release_bookmark( $name ): bool {
}

unset( $this->bookmarks[ $name ] );
unset( $this->bookmarks_with_skipped_newline[ $name ] );

return true;
}
Expand Down Expand Up @@ -2517,7 +2531,9 @@ private function apply_attributes_updates( int $shift_this_point ): int {
return 0;
}

$accumulated_shift_for_given_point = 0;
$accumulated_shift_for_given_point = 0;
$accumulated_shift_for_skip_newline = 0;
$skip_newline_at = $this->skip_newline_at;

/*
* Attribute updates can be enqueued in any order but updates
Expand All @@ -2541,6 +2557,10 @@ private function apply_attributes_updates( int $shift_this_point ): int {
$this->bytes_already_parsed += $shift;
}

if ( null !== $skip_newline_at && $diff->start < $skip_newline_at ) {
$accumulated_shift_for_skip_newline += $shift;
}

// Accumulate shift of the given pointer within this function call.
if ( $diff->start < $shift_this_point ) {
$accumulated_shift_for_given_point += $shift;
Expand All @@ -2551,6 +2571,10 @@ private function apply_attributes_updates( int $shift_this_point ): int {
$bytes_already_copied = $diff->start + $diff->length;
}

if ( null !== $skip_newline_at ) {
$this->skip_newline_at += $accumulated_shift_for_skip_newline;
}

$this->html = $output_buffer . substr( $this->html, $bytes_already_copied );

/*
Expand Down Expand Up @@ -2583,8 +2607,11 @@ private function apply_attributes_updates( int $shift_this_point ): int {

$delta = strlen( $diff->text ) - $diff->length;

if ( $bookmark->start >= $diff->start ) {
if ( $bookmark->start > $diff->start || ( $bookmark->start === $diff->start && 0 === $diff->length ) ) {
$head_delta += $delta;
if ( $bookmark->start === $diff->start && 0 === $diff->length && '' !== $diff->text ) {
unset( $this->bookmarks_with_skipped_newline[ $bookmark_name ] );
}
}

if ( $bookmark_end >= $diff_end ) {
Expand Down Expand Up @@ -2657,6 +2684,9 @@ public function seek( $bookmark_name ): bool {

// Point this tag processor before the sought tag opener and consume it.
$this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start;
$this->skip_newline_at = isset( $this->bookmarks_with_skipped_newline[ $bookmark_name ] )
? $this->bytes_already_parsed
: null;
$this->parser_state = self::STATE_READY;
return $this->next_token();
}
Expand Down Expand Up @@ -3636,13 +3666,6 @@ public function subdivide_text_appropriately(): bool {
* that a token has modifiable text, and a token with modifiable text may
* have an empty string (e.g. a comment with no contents).
*
* Limitations:
*
* - This function will not strip the leading newline appropriately
* after seeking into a LISTING or PRE element. To ensure that the
* newline is treated properly, seek to the LISTING or PRE opening
* tag instead of to the first text node inside the element.
*
* @since 6.5.0
* @since 6.7.0 Replaces NULL bytes (U+0000) and newlines appropriately.
*
Expand Down
31 changes: 31 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,37 @@
* @coversDefaultClass WP_HTML_Processor
*/
class Tests_HtmlApi_WpHtmlProcessorModifiableText extends WP_UnitTestCase {
/**
* Ensures that bookmarked text remains seekable after updating the same text node.
*/
public function test_seeks_to_bookmarked_text_after_modifiable_text_update(): void {
$processor = WP_HTML_Processor::create_fragment( "<pre>\nabc</pre><span>" );

$this->assertTrue( $processor->next_token(), 'Should have found the PRE opener.' );
$this->assertSame( 'PRE', $processor->get_token_name(), 'Should have found the PRE opener: check test setup.' );

while ( $processor->next_token() && 'abc' !== $processor->get_modifiable_text() ) {
continue;
}

$this->assertSame( '#text', $processor->get_token_name(), 'Should have found the PRE text node: check test setup.' );
$this->assertSame( 'abc', $processor->get_modifiable_text(), 'Should have stripped the leading newline from the PRE text on first traversal.' );
$this->assertTrue( $processor->set_bookmark( 'text' ), 'Should have bookmarked the PRE text node.' );

$this->assertTrue( $processor->set_modifiable_text( 'xyz' ), 'Should have updated the PRE text node.' );
$this->assertTrue( $processor->next_token(), 'Should have advanced away from the bookmarked text node.' );
$this->assertSame( 'PRE', $processor->get_token_name(), 'Should have advanced to the PRE closer: check test setup.' );
$this->assertSame( "<pre>\nxyz</pre><span>", $processor->get_updated_html(), 'Should have updated the PRE text node.' );

$this->assertTrue( $processor->seek( 'text' ), 'Should have sought back to the updated PRE text node.' );
$this->assertSame( '#text', $processor->get_token_name(), 'Should have sought back to the text node.' );
$this->assertSame(
'xyz',
$processor->get_modifiable_text(),
'Should have replayed the updated PRE text node after seeking.'
);
}

/**
* TEXTAREA elements ignore the first newline in their content.
* Setting the modifiable text with a leading newline (or carriage return variants)
Expand Down
41 changes: 41 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlTagProcessor-bookmark.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,47 @@ public function test_updates_bookmark_for_deletions_before_both_sides() {
);
}

/**
* Ensures that bookmarks stay attached to their original token when text is inserted at the bookmark start.
*
* @ticket 56299
*
* @covers WP_HTML_Tag_Processor::seek
*/
public function test_updates_bookmark_for_insertions_at_start() {
$processor = new class( '<div></div><span></span>' ) extends WP_HTML_Tag_Processor {
/**
* Inserts HTML at the start of the given bookmark.
*
* @param string $bookmark_name Bookmark name.
* @param string $html HTML to insert.
*/
public function insert_html_at_bookmark_start( string $bookmark_name, string $html ): void {
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
$this->bookmarks[ $bookmark_name ]->start,
0,
$html
);
}
};

$processor->next_tag( 'SPAN' );
$processor->set_bookmark( 'target' );
$processor->insert_html_at_bookmark_start( 'target', '<p></p>' );

$this->assertSame(
'<div></div><p></p><span></span>',
$processor->get_updated_html(),
'Should have inserted the HTML at the bookmark start.'
);
$this->assertTrue( $processor->seek( 'target' ), 'Should have sought back to the original bookmarked token.' );
$this->assertSame(
'SPAN',
$processor->get_token_name(),
'Should have kept the bookmark attached to the original SPAN token after insertion.'
);
}

/**
* @ticket 56299
*
Expand Down
Loading
Loading