From d19a6bc1fad4752f183ff6d29a9ccc7d0ce25072 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 5 Feb 2026 18:35:23 +0100 Subject: [PATCH 01/18] Add tests --- .../wpHtmlProcessorModifiableText.php | 85 +++++++++++++++++++ .../wpHtmlTagProcessorModifiableText.php | 70 +++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php new file mode 100644 index 0000000000000..42ebca3310296 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -0,0 +1,85 @@ +' ); + $processor->next_token(); + $processor->set_modifiable_text( "\nAFTER NEWLINE" ); + $this->assertSame( + "\nAFTER NEWLINE", + $processor->get_modifiable_text(), + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } + + /** + * PRE elements ignore the first newline in their content. + * Setting the modifiable text with a leading newline should ensure that the leading newline + * is present in the resulting TEXTAREA. + * + * @todo Leading whitespace mage split into multiple text nodes. Add appropriate tests. + * + * @ticket 64607 + */ + public function test_modifiable_text_special_pre() { + $set_text = "\nAFTER NEWLINE"; + $processor = WP_HTML_Processor::create_fragment( '
REPLACEME
' ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } + + /** + * LISTING elements ignore the first newline in their content. + * Setting the modifiable text with a leading newline should ensure that the leading newline + * is present in the resulting TEXTAREA. + * + * @todo Leading whitespace mage split into multiple text nodes. Add appropriate tests. + * + * @ticket 64607 + */ + public function test_modifiable_text_special_listing() { + $set_text = "\nAFTER NEWLINE"; + $processor = WP_HTML_Processor::create_fragment( 'REPLACEME' ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 9e0d94aecd17e..a021675b7edd1 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -636,4 +636,74 @@ public function test_json_auto_escaping() { $decoded_json_from_html ); } + + /** + * TEXTAREA elements ignore the first newline in their content. + * Setting the modifiable text with a leading newline should ensure that the leading newline + * is present in the resulting TEXTAREA. + * + * @ticket 64607 + */ + public function test_modifiable_text_special_textarea() { + $processor = new WP_HTML_Tag_Processor( '' ); + $processor->next_token(); + $processor->set_modifiable_text( "\nAFTER NEWLINE" ); + $this->assertSame( + "\nAFTER NEWLINE", + $processor->get_modifiable_text(), + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } + + /** + * PRE elements ignore the first newline in their content. + * Setting the modifiable text with a leading newline should ensure that the leading newline + * is present in the resulting TEXTAREA. + * + * @ticket 64607 + */ + public function test_modifiable_text_special_pre() { + $set_text = "\nAFTER NEWLINE"; + $processor = new WP_HTML_Tag_Processor( '
REPLACEME
' ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } + + /** + * LISTING elements ignore the first newline in their content. + * Setting the modifiable text with a leading newline should ensure that the leading newline + * is present in the resulting TEXTAREA. + * + * @ticket 64607 + */ + public function test_modifiable_text_special_listing() { + $set_text = "\nAFTER NEWLINE"; + $processor = new WP_HTML_Tag_Processor( 'REPLACEME' ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } } From 78652d03fac68a32d355fe2591878e00e14c6b3c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 5 Feb 2026 19:09:34 +0100 Subject: [PATCH 02/18] Add set_modifiable_text_fixes --- .../html-api/class-wp-html-tag-processor.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 69e3e5d2c7557..65fd17bd1f034 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3770,6 +3770,16 @@ public function get_modifiable_text(): string { */ public function set_modifiable_text( string $plaintext_content ): bool { if ( self::STATE_TEXT_NODE === $this->parser_state ) { + /* + * In case the text starts at a position where a newline is skipped _and_ starts + * with a newline, add an additional newline. + * This preserves the intention of adding text with a leading newline which would + * be removed in HTML. + */ + if ( $this->skip_newline_at === $this->text_starts_at && str_starts_with( $plaintext_content, "\n" ) ) { + $plaintext_content = "\n{$plaintext_content}"; + } + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( $this->text_starts_at, $this->text_length, @@ -3876,6 +3886,14 @@ static function ( $tag_match ) { }, $plaintext_content ); + /* + * A single leading newline will be removed from TEXTAREA contents, if present. + * If the intention is to start with a leading newline, ensure that it is preserved + * by adding an additional leading newline. + */ + if ( 'TEXTAREA' === $this->get_tag() && str_starts_with( $plaintext_content, "\n" ) ) { + $plaintext_content = "\n{$plaintext_content}"; + } /* * These don't _need_ to be escaped, but since they are decoded it's From 53197dff53aef3bb6352512513266a11c1db3269 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 5 Feb 2026 19:44:28 +0100 Subject: [PATCH 03/18] Test working correctly in multiple cases @todo split into data provider --- .../wpHtmlProcessorModifiableText.php | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 42ebca3310296..5003d87285c6d 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -32,8 +32,6 @@ public function test_modifiable_text_special_textarea() { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting TEXTAREA. * - * @todo Leading whitespace mage split into multiple text nodes. Add appropriate tests. - * * @ticket 64607 */ public function test_modifiable_text_special_pre() { @@ -55,6 +53,87 @@ public function test_modifiable_text_special_pre() { ); } + /** + * + * @ticket 64607 + */ + public function test_modifiable_text_special_pre_leading_whitespace() { + $set_text = "\nAFTER NEWLINE."; + $processor = WP_HTML_Processor::create_fragment( "
\nREPLACEME
" ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + // This is an empty text node because of how the HTML Processor works. + $this->assertSame( '', $processor->get_modifiable_text() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text}REPLACEME + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + + $processor = WP_HTML_Processor::create_fragment( "
\nREPLACEME
" ); + $processor->next_tag(); + $processor->next_token(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + // This is an empty text node because of how the HTML Processor works. + $this->assertSame( 'REPLACEME', $processor->get_modifiable_text() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + + $processor = WP_HTML_Processor::create_fragment( '
 REPLACEME
' ); + $processor->next_tag(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + // This is an empty text node because of how the HTML Processor works. + $this->assertSame( ' ', $processor->get_modifiable_text() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text}REPLACEME + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + + $processor = WP_HTML_Processor::create_fragment( '
 REPLACEME
' ); + $processor->next_tag(); + $processor->next_token(); + $processor->next_token(); + $this->assertSame( '#text', $processor->get_token_type() ); + // This is an empty text node because of how the HTML Processor works. + $this->assertSame( 'REPLACEME', $processor->get_modifiable_text() ); + $processor->set_modifiable_text( $set_text ); + $this->assertSame( $set_text, $processor->get_modifiable_text() ); + $this->assertEqualHTML( + << + {$set_text} + HTML, + $processor->get_updated_html(), + '', + 'Should have preserved the leading newline in the TEXTAREA content.' + ); + } + /** * LISTING elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline From 886ad101d8d0199b9cb9a3ccc144345bb7b52a3c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 5 Feb 2026 19:51:03 +0100 Subject: [PATCH 04/18] align lint --- .../tests/html-api/wpHtmlProcessorModifiableText.php | 6 +++--- .../tests/html-api/wpHtmlTagProcessorModifiableText.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 5003d87285c6d..01af358fa1b23 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -35,7 +35,7 @@ public function test_modifiable_text_special_textarea() { * @ticket 64607 */ public function test_modifiable_text_special_pre() { - $set_text = "\nAFTER NEWLINE"; + $set_text = "\nAFTER NEWLINE"; $processor = WP_HTML_Processor::create_fragment( '
REPLACEME
' ); $processor->next_tag(); $processor->next_token(); @@ -58,7 +58,7 @@ public function test_modifiable_text_special_pre() { * @ticket 64607 */ public function test_modifiable_text_special_pre_leading_whitespace() { - $set_text = "\nAFTER NEWLINE."; + $set_text = "\nAFTER NEWLINE."; $processor = WP_HTML_Processor::create_fragment( "
\nREPLACEME
" ); $processor->next_tag(); $processor->next_token(); @@ -144,7 +144,7 @@ public function test_modifiable_text_special_pre_leading_whitespace() { * @ticket 64607 */ public function test_modifiable_text_special_listing() { - $set_text = "\nAFTER NEWLINE"; + $set_text = "\nAFTER NEWLINE"; $processor = WP_HTML_Processor::create_fragment( 'REPLACEME' ); $processor->next_tag(); $processor->next_token(); diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index a021675b7edd1..16f55f6281e4e 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -663,7 +663,7 @@ public function test_modifiable_text_special_textarea() { * @ticket 64607 */ public function test_modifiable_text_special_pre() { - $set_text = "\nAFTER NEWLINE"; + $set_text = "\nAFTER NEWLINE"; $processor = new WP_HTML_Tag_Processor( '
REPLACEME
' ); $processor->next_tag(); $processor->next_token(); @@ -689,7 +689,7 @@ public function test_modifiable_text_special_pre() { * @ticket 64607 */ public function test_modifiable_text_special_listing() { - $set_text = "\nAFTER NEWLINE"; + $set_text = "\nAFTER NEWLINE"; $processor = new WP_HTML_Tag_Processor( 'REPLACEME' ); $processor->next_tag(); $processor->next_token(); From 15f675d8165db3ab9e4d1aae41485e8fce183b93 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 10:58:51 +0100 Subject: [PATCH 05/18] Use a data provider over of repeat tests and steps --- .../wpHtmlProcessorModifiableText.php | 152 +++++++----------- 1 file changed, 56 insertions(+), 96 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 01af358fa1b23..941c4f43b5a33 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -28,15 +28,19 @@ public function test_modifiable_text_special_textarea() { } /** - * PRE elements ignore the first newline in their content. + * PRE and LISTING elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. + * is present in the resulting element. * * @ticket 64607 + * + * @dataProvider data_modifiable_text_special_pre_tags + * + * @param string $tag_name The tag name to test (e.g. 'pre', 'listing'). */ - public function test_modifiable_text_special_pre() { + public function test_modifiable_text_special_pre_tags( string $tag_name ) { $set_text = "\nAFTER NEWLINE"; - $processor = WP_HTML_Processor::create_fragment( '
REPLACEME
' ); + $processor = WP_HTML_Processor::create_fragment( "<{$tag_name}>REPLACEME" ); $processor->next_tag(); $processor->next_token(); $this->assertSame( '#text', $processor->get_token_type() ); @@ -44,121 +48,77 @@ public function test_modifiable_text_special_pre() { $this->assertSame( $set_text, $processor->get_modifiable_text() ); $this->assertEqualHTML( << - {$set_text} + <{$tag_name}> + {$set_text} HTML, $processor->get_updated_html(), '', - 'Should have preserved the leading newline in the TEXTAREA content.' + "Should have preserved the leading newline in the {$tag_name} content." ); } /** + * Data provider. * - * @ticket 64607 + * @return array[] */ - public function test_modifiable_text_special_pre_leading_whitespace() { - $set_text = "\nAFTER NEWLINE."; - $processor = WP_HTML_Processor::create_fragment( "
\nREPLACEME
" ); - $processor->next_tag(); - $processor->next_token(); - $this->assertSame( '#text', $processor->get_token_type() ); - // This is an empty text node because of how the HTML Processor works. - $this->assertSame( '', $processor->get_modifiable_text() ); - $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); - $this->assertEqualHTML( - << - {$set_text}REPLACEME - HTML, - $processor->get_updated_html(), - '', - 'Should have preserved the leading newline in the TEXTAREA content.' - ); - - $processor = WP_HTML_Processor::create_fragment( "
\nREPLACEME
" ); - $processor->next_tag(); - $processor->next_token(); - $processor->next_token(); - $this->assertSame( '#text', $processor->get_token_type() ); - // This is an empty text node because of how the HTML Processor works. - $this->assertSame( 'REPLACEME', $processor->get_modifiable_text() ); - $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); - $this->assertEqualHTML( - << - {$set_text} - HTML, - $processor->get_updated_html(), - '', - 'Should have preserved the leading newline in the TEXTAREA content.' - ); - - $processor = WP_HTML_Processor::create_fragment( '
 REPLACEME
' ); - $processor->next_tag(); - $processor->next_token(); - $this->assertSame( '#text', $processor->get_token_type() ); - // This is an empty text node because of how the HTML Processor works. - $this->assertSame( ' ', $processor->get_modifiable_text() ); - $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); - $this->assertEqualHTML( - << - {$set_text}REPLACEME - HTML, - $processor->get_updated_html(), - '', - 'Should have preserved the leading newline in the TEXTAREA content.' - ); - - $processor = WP_HTML_Processor::create_fragment( '
 REPLACEME
' ); - $processor->next_tag(); - $processor->next_token(); - $processor->next_token(); - $this->assertSame( '#text', $processor->get_token_type() ); - // This is an empty text node because of how the HTML Processor works. - $this->assertSame( 'REPLACEME', $processor->get_modifiable_text() ); - $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); - $this->assertEqualHTML( - << - {$set_text} - HTML, - $processor->get_updated_html(), - '', - 'Should have preserved the leading newline in the TEXTAREA content.' + public static function data_modifiable_text_special_pre_tags() { + return array( + 'PRE' => array( 'pre' ), + 'LISTING' => array( 'listing' ), ); } /** - * LISTING elements ignore the first newline in their content. - * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. - * - * @todo Leading whitespace mage split into multiple text nodes. Add appropriate tests. + * PRE and LISTING elements ignore the first newline in their content. + * Leading whitespace may split into multiple text nodes in the HTML Processor. + * Setting the modifiable text with a leading newline should ensure that the + * leading newline is present in the resulting element. * * @ticket 64607 + * + * @dataProvider data_modifiable_text_special_leading_whitespace + * + * @param string $html HTML containing the element to test. + * @param int $advance_n_tokens Count of times to run `next_token()` after `next_tag()`. + * @param string $initial_text Expected modifiable text before the update. + * @param string $expected_html Expected HTML output after setting modifiable text. */ - public function test_modifiable_text_special_listing() { - $set_text = "\nAFTER NEWLINE"; - $processor = WP_HTML_Processor::create_fragment( 'REPLACEME' ); + public function test_modifiable_text_special_leading_whitespace( string $html, int $advance_n_tokens, string $initial_text, string $expected_html ) { + $set_text = "\nAFTER NEWLINE."; + $processor = WP_HTML_Processor::create_fragment( $html ); $processor->next_tag(); - $processor->next_token(); + while ( --$advance_n_tokens >= 0 ) { + $processor->next_token(); + } $this->assertSame( '#text', $processor->get_token_type() ); + $this->assertSame( $initial_text, $processor->get_modifiable_text() ); $processor->set_modifiable_text( $set_text ); $this->assertSame( $set_text, $processor->get_modifiable_text() ); $this->assertEqualHTML( - << - {$set_text} - HTML, + $expected_html, $processor->get_updated_html(), '', - 'Should have preserved the leading newline in the TEXTAREA content.' + 'Should have preserved the leading newline in the element content.' ); } + + /** + * Data provider. + * + * @return Generator + */ + public static function data_modifiable_text_special_leading_whitespace() { + $set_text = "\nAFTER NEWLINE."; + + foreach ( self::data_modifiable_text_special_pre_tags() as $tag_data ) { + $tag_name = $tag_data[0]; + $TAG = strtoupper( $tag_name ); + + yield "{$TAG} with leading newline, first text node" => array( "<{$tag_name}>\nREPLACEME", 1, '', "<{$tag_name}>\n{$set_text}REPLACEME" ); + yield "{$TAG} with leading newline, second text node" => array( "<{$tag_name}>\nREPLACEME", 2, 'REPLACEME', "<{$tag_name}>\n{$set_text}" ); + yield "{$TAG} with leading space, first text node" => array( "<{$tag_name}> REPLACEME", 1, ' ', "<{$tag_name}>\n{$set_text}REPLACEME" ); + yield "{$TAG} with leading space, second text node" => array( "<{$tag_name}> REPLACEME", 2, 'REPLACEME', "<{$tag_name}>\n {$set_text}" ); + } + } } From 40136f683b1b00eb0ad6a5d9fedd8ce95eda883a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 11:02:14 +0100 Subject: [PATCH 06/18] Fix lint, clean up yields --- .../wpHtmlProcessorModifiableText.php | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 941c4f43b5a33..6912470ceb844 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -105,20 +105,38 @@ public function test_modifiable_text_special_leading_whitespace( string $html, i /** * Data provider. - * - * @return Generator */ public static function data_modifiable_text_special_leading_whitespace() { $set_text = "\nAFTER NEWLINE."; foreach ( self::data_modifiable_text_special_pre_tags() as $tag_data ) { - $tag_name = $tag_data[0]; - $TAG = strtoupper( $tag_name ); + $tag_name = $tag_data[0]; + $tag_label = strtoupper( $tag_name ); - yield "{$TAG} with leading newline, first text node" => array( "<{$tag_name}>\nREPLACEME", 1, '', "<{$tag_name}>\n{$set_text}REPLACEME" ); - yield "{$TAG} with leading newline, second text node" => array( "<{$tag_name}>\nREPLACEME", 2, 'REPLACEME', "<{$tag_name}>\n{$set_text}" ); - yield "{$TAG} with leading space, first text node" => array( "<{$tag_name}> REPLACEME", 1, ' ', "<{$tag_name}>\n{$set_text}REPLACEME" ); - yield "{$TAG} with leading space, second text node" => array( "<{$tag_name}> REPLACEME", 2, 'REPLACEME', "<{$tag_name}>\n {$set_text}" ); + yield "{$tag_label} with leading newline, first text node" => array( + "<{$tag_name}>\nREPLACEME", + 1, + '', + "<{$tag_name}>\n{$set_text}REPLACEME", + ); + yield "{$tag_label} with leading newline, second text node" => array( + "<{$tag_name}>\nREPLACEME", + 2, + 'REPLACEME', + "<{$tag_name}>\n{$set_text}", + ); + yield "{$tag_label} with leading space, first text node" => array( + "<{$tag_name}> REPLACEME", + 1, + ' ', + "<{$tag_name}>\n{$set_text}REPLACEME", + ); + yield "{$tag_label} with leading space, second text node" => array( + "<{$tag_name}> REPLACEME", + 2, + 'REPLACEME', + "<{$tag_name}>\n {$set_text}", + ); } } } From 37e3597ed6a47ce747a2935861aa5d7862c6d596 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 11:04:19 +0100 Subject: [PATCH 07/18] Add test explanation --- .../phpunit/tests/html-api/wpHtmlProcessorModifiableText.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 6912470ceb844..35ed72769a12b 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -70,6 +70,10 @@ public static function data_modifiable_text_special_pre_tags() { } /** + * The HTML Processor has special behavior when a text node starts with whitespace. + * Test that PRE and LISTING `::set_modifiable_text()` handling works correctly + * with leading whitespace. + * * PRE and LISTING elements ignore the first newline in their content. * Leading whitespace may split into multiple text nodes in the HTML Processor. * Setting the modifiable text with a leading newline should ensure that the From 4b2a7c9959f684c00e25cf97e3d9bacb6d151b3f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 12:38:49 +0100 Subject: [PATCH 08/18] Add carriage return tests --- .../wpHtmlProcessorModifiableText.php | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 35ed72769a12b..7a781b5ee0d3b 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -83,22 +83,39 @@ public static function data_modifiable_text_special_pre_tags() { * * @dataProvider data_modifiable_text_special_leading_whitespace * - * @param string $html HTML containing the element to test. + * @param string $html HTML containing the element to test. * @param int $advance_n_tokens Count of times to run `next_token()` after `next_tag()`. - * @param string $initial_text Expected modifiable text before the update. - * @param string $expected_html Expected HTML output after setting modifiable text. + * @param string $stopped_on_text Expected modifiable text before the update. + * @param string $set_text Text to set. + * @param string $expected_html Expected HTML output after setting modifiable text. */ - public function test_modifiable_text_special_leading_whitespace( string $html, int $advance_n_tokens, string $initial_text, string $expected_html ) { - $set_text = "\nAFTER NEWLINE."; + public function test_modifiable_text_special_leading_whitespace( + string $html, + int $advance_n_tokens, + string $stopped_on_text, + string $set_text, + string $expected_html + ) { $processor = WP_HTML_Processor::create_fragment( $html ); $processor->next_tag(); while ( --$advance_n_tokens >= 0 ) { $processor->next_token(); } $this->assertSame( '#text', $processor->get_token_type() ); - $this->assertSame( $initial_text, $processor->get_modifiable_text() ); + $this->assertSame( $stopped_on_text, $processor->get_modifiable_text() ); $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); + + // Newline normalization transforms \r and \r\n into \n. + $this->assertSame( + strtr( + $set_text, + array( + "\r\n" => "\n", + "\r" => "\n", + ) + ), + $processor->get_modifiable_text() + ); $this->assertEqualHTML( $expected_html, $processor->get_updated_html(), @@ -121,26 +138,49 @@ public static function data_modifiable_text_special_leading_whitespace() { "<{$tag_name}>\nREPLACEME", 1, '', + $set_text, "<{$tag_name}>\n{$set_text}REPLACEME", ); + yield "{$tag_label} with leading newline, second text node" => array( "<{$tag_name}>\nREPLACEME", 2, 'REPLACEME', + $set_text, "<{$tag_name}>\n{$set_text}", ); + yield "{$tag_label} with leading space, first text node" => array( "<{$tag_name}> REPLACEME", 1, ' ', + $set_text, "<{$tag_name}>\n{$set_text}REPLACEME", ); + yield "{$tag_label} with leading space, second text node" => array( "<{$tag_name}> REPLACEME", 2, 'REPLACEME', + $set_text, "<{$tag_name}>\n {$set_text}", ); + + yield "{$tag_label} insert with leading carriage return" => array( + "<{$tag_name}>REPLACEME", + 1, + 'REPLACEME', + "\rCR", + "<{$tag_name}>\n\nCR", + ); + + yield "{$tag_label} insert with leading carriage return + newline" => array( + "<{$tag_name}>REPLACEME", + 1, + 'REPLACEME', + "\r\nCR-N", + "<{$tag_name}>\n\nCR-N", + ); } } } From a945a14a3904b20f4003e8211174f13a5d934a9d Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 12:40:14 +0100 Subject: [PATCH 09/18] Add clearing text test --- .../tests/html-api/wpHtmlProcessorModifiableText.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 7a781b5ee0d3b..40627b76fa5e1 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -181,6 +181,14 @@ public static function data_modifiable_text_special_leading_whitespace() { "\r\nCR-N", "<{$tag_name}>\n\nCR-N", ); + + yield "{$tag_label} clear text" => array( + "<{$tag_name}>REPLACEME", + 1, + 'REPLACEME', + '', + "<{$tag_name}>", + ); } } } From 5c5dff2d91378b73522036fbad45c33fc35f87d1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 12:40:25 +0100 Subject: [PATCH 10/18] Handle carriage returns --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 65fd17bd1f034..d12f55c87e477 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3776,7 +3776,10 @@ public function set_modifiable_text( string $plaintext_content ): bool { * This preserves the intention of adding text with a leading newline which would * be removed in HTML. */ - if ( $this->skip_newline_at === $this->text_starts_at && str_starts_with( $plaintext_content, "\n" ) ) { + if ( + $this->skip_newline_at === $this->text_starts_at && + 1 === strspn( $plaintext_content, "\n\r", 0, 1 ) + ) { $plaintext_content = "\n{$plaintext_content}"; } From b7ddbb468c0a112efe5779cef3bff26c086c08cd Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 12:46:38 +0100 Subject: [PATCH 11/18] Fix incorrect element names in tests --- .../html-api/wpHtmlTagProcessorModifiableText.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 16f55f6281e4e..1c977b54f8c44 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -640,7 +640,7 @@ public function test_json_auto_escaping() { /** * TEXTAREA elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. + * is present in the resulting element. * * @ticket 64607 */ @@ -651,14 +651,14 @@ public function test_modifiable_text_special_textarea() { $this->assertSame( "\nAFTER NEWLINE", $processor->get_modifiable_text(), - 'Should have preserved the leading newline in the TEXTAREA content.' + 'Should have preserved the leading newline in the content.' ); } /** * PRE elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. + * is present in the resulting element. * * @ticket 64607 */ @@ -677,14 +677,14 @@ public function test_modifiable_text_special_pre() { HTML, $processor->get_updated_html(), '', - 'Should have preserved the leading newline in the TEXTAREA content.' + 'Should have preserved the leading newline in the content.' ); } /** * LISTING elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. + * is present in the resulting element. * * @ticket 64607 */ @@ -703,7 +703,7 @@ public function test_modifiable_text_special_listing() { HTML, $processor->get_updated_html(), '', - 'Should have preserved the leading newline in the TEXTAREA content.' + 'Should have preserved the leading newline in the content.' ); } } From 6afd8fe900bd46ca824c0a1729f851401fa428f0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 13:59:30 +0100 Subject: [PATCH 12/18] Update ticket number --- .../tests/html-api/wpHtmlProcessorModifiableText.php | 6 +++--- .../tests/html-api/wpHtmlTagProcessorModifiableText.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 40627b76fa5e1..3e24f9d586636 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -14,7 +14,7 @@ class Tests_HtmlApi_WpHtmlProcessorModifiableText extends WP_UnitTestCase { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting TEXTAREA. * - * @ticket 64607 + * @ticket 64609 */ public function test_modifiable_text_special_textarea() { $processor = WP_HTML_Processor::create_fragment( '' ); @@ -32,7 +32,7 @@ public function test_modifiable_text_special_textarea() { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting element. * - * @ticket 64607 + * @ticket 64609 * * @dataProvider data_modifiable_text_special_pre_tags * @@ -79,7 +79,7 @@ public static function data_modifiable_text_special_pre_tags() { * Setting the modifiable text with a leading newline should ensure that the * leading newline is present in the resulting element. * - * @ticket 64607 + * @ticket 64609 * * @dataProvider data_modifiable_text_special_leading_whitespace * diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 1c977b54f8c44..b4262f80ea3d9 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -642,7 +642,7 @@ public function test_json_auto_escaping() { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting element. * - * @ticket 64607 + * @ticket 64609 */ public function test_modifiable_text_special_textarea() { $processor = new WP_HTML_Tag_Processor( '' ); @@ -660,7 +660,7 @@ public function test_modifiable_text_special_textarea() { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting element. * - * @ticket 64607 + * @ticket 64609 */ public function test_modifiable_text_special_pre() { $set_text = "\nAFTER NEWLINE"; @@ -686,7 +686,7 @@ public function test_modifiable_text_special_pre() { * Setting the modifiable text with a leading newline should ensure that the leading newline * is present in the resulting element. * - * @ticket 64607 + * @ticket 64609 */ public function test_modifiable_text_special_listing() { $set_text = "\nAFTER NEWLINE"; From 053dccfffd8c9d5182ac4b2d29d15cd95c9d4f7c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 14:09:15 +0100 Subject: [PATCH 13/18] Add carriage return tests --- .../wpHtmlProcessorModifiableText.php | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 3e24f9d586636..4d7eab0f90867 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -27,6 +27,56 @@ public function test_modifiable_text_special_textarea() { ); } + /** + * TEXTAREA elements ignore the first newline in their content. + * Setting the modifiable text with a leading carriage return should be normalized + * and ensure the leading newline is present in the resulting TEXTAREA. + * + * @ticket 64609 + */ + public function test_modifiable_text_special_textarea_carriage_return() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_token(); + $processor->set_modifiable_text( "\rCR" ); + // Newline normalization transforms \r into \n, and special handling should preserve it. + $this->assertSame( + "\nCR", + $processor->get_modifiable_text(), + 'Should have normalized carriage return and preserved the leading newline in the TEXTAREA content.' + ); + $this->assertEqualHTML( + "", + $processor->get_updated_html(), + '', + 'Should have doubled the newline in the output HTML to preserve the leading newline.' + ); + } + + /** + * TEXTAREA elements ignore the first newline in their content. + * Setting the modifiable text with a leading carriage return + newline should be normalized + * and ensure the leading newline is present in the resulting TEXTAREA. + * + * @ticket 64609 + */ + public function test_modifiable_text_special_textarea_carriage_return_newline() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_token(); + $processor->set_modifiable_text( "\r\nCR-N" ); + // Newline normalization transforms \r\n into \n, and special handling should preserve it. + $this->assertSame( + "\nCR-N", + $processor->get_modifiable_text(), + 'Should have normalized carriage return + newline and preserved the leading newline in the TEXTAREA content.' + ); + $this->assertEqualHTML( + "", + $processor->get_updated_html(), + '', + 'Should have doubled the newline in the output HTML to preserve the leading newline.' + ); + } + /** * PRE and LISTING elements ignore the first newline in their content. * Setting the modifiable text with a leading newline should ensure that the leading newline From 960638b926c3a7ef4c188ca7ed610b472fc6ce90 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 14:09:32 +0100 Subject: [PATCH 14/18] Fix carriage return handling in TEXTAREA --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index d12f55c87e477..ee6f6ef9b9e1c 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3894,7 +3894,10 @@ static function ( $tag_match ) { * If the intention is to start with a leading newline, ensure that it is preserved * by adding an additional leading newline. */ - if ( 'TEXTAREA' === $this->get_tag() && str_starts_with( $plaintext_content, "\n" ) ) { + if ( + 'TEXTAREA' === $this->get_tag() && + 1 === strspn( $plaintext_content, "\n\r", 0, 1 ) + ) { $plaintext_content = "\n{$plaintext_content}"; } From b61fc6d869bdd59a51cc42274a1e496c3e0c7def Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 15:00:27 +0100 Subject: [PATCH 15/18] Use data provider for repeat tests --- .../wpHtmlProcessorModifiableText.php | 79 ++++++++----------- 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 4d7eab0f90867..2e138bbc38bfa 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -11,69 +11,58 @@ class Tests_HtmlApi_WpHtmlProcessorModifiableText extends WP_UnitTestCase { /** * TEXTAREA elements ignore the first newline in their content. - * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting TEXTAREA. + * Setting the modifiable text with a leading newline (or carriage return variants) + * should ensure that the leading newline is present in the resulting TEXTAREA. * * @ticket 64609 - */ - public function test_modifiable_text_special_textarea() { - $processor = WP_HTML_Processor::create_fragment( '' ); - $processor->next_token(); - $processor->set_modifiable_text( "\nAFTER NEWLINE" ); - $this->assertSame( - "\nAFTER NEWLINE", - $processor->get_modifiable_text(), - 'Should have preserved the leading newline in the TEXTAREA content.' - ); - } - - /** - * TEXTAREA elements ignore the first newline in their content. - * Setting the modifiable text with a leading carriage return should be normalized - * and ensure the leading newline is present in the resulting TEXTAREA. * - * @ticket 64609 + * @dataProvider data_modifiable_text_special_textarea + * + * @param string $set_text Text to set. + * @param string $expected_html Expected HTML output. */ - public function test_modifiable_text_special_textarea_carriage_return() { + public function test_modifiable_text_special_textarea( string $set_text, string $expected_html ) { $processor = WP_HTML_Processor::create_fragment( '' ); $processor->next_token(); - $processor->set_modifiable_text( "\rCR" ); - // Newline normalization transforms \r into \n, and special handling should preserve it. + $processor->set_modifiable_text( $set_text ); $this->assertSame( - "\nCR", + strtr( + $set_text, + array( + "\r\n" => "\n", + "\r" => "\n", + ) + ), $processor->get_modifiable_text(), - 'Should have normalized carriage return and preserved the leading newline in the TEXTAREA content.' + 'Should have preserved or normalized the leading newline in the TEXTAREA content.' ); $this->assertEqualHTML( - "", + $expected_html, $processor->get_updated_html(), '', - 'Should have doubled the newline in the output HTML to preserve the leading newline.' + 'Should have correctly output the TEXTAREA HTML.' ); } /** - * TEXTAREA elements ignore the first newline in their content. - * Setting the modifiable text with a leading carriage return + newline should be normalized - * and ensure the leading newline is present in the resulting TEXTAREA. + * Data provider. * - * @ticket 64609 + * @return array[] */ - public function test_modifiable_text_special_textarea_carriage_return_newline() { - $processor = WP_HTML_Processor::create_fragment( '' ); - $processor->next_token(); - $processor->set_modifiable_text( "\r\nCR-N" ); - // Newline normalization transforms \r\n into \n, and special handling should preserve it. - $this->assertSame( - "\nCR-N", - $processor->get_modifiable_text(), - 'Should have normalized carriage return + newline and preserved the leading newline in the TEXTAREA content.' - ); - $this->assertEqualHTML( - "", - $processor->get_updated_html(), - '', - 'Should have doubled the newline in the output HTML to preserve the leading newline.' + public static function data_modifiable_text_special_textarea() { + return array( + 'Leading newline' => array( + "\nAFTER NEWLINE", + "", + ), + 'Leading carriage return' => array( + "\rCR", + "", + ), + 'Leading carriage return + newline' => array( + "\r\nCR-N", + "", + ), ); } From 954dbf57d4d20f608ff31497e48d8efa27492f4e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 15:14:40 +0100 Subject: [PATCH 16/18] Further simplify tests --- .../wpHtmlProcessorModifiableText.php | 102 ++++++------------ 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index 2e138bbc38bfa..d7f6e33785224 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -14,6 +14,10 @@ class Tests_HtmlApi_WpHtmlProcessorModifiableText extends WP_UnitTestCase { * Setting the modifiable text with a leading newline (or carriage return variants) * should ensure that the leading newline is present in the resulting TEXTAREA. * + * TEXTAREA are treated as atomic tags by the tag processor, so `set_modifiable_text()` + * is called directly on the TEXTAREA token, making them different from PRE and LISTING + * tags that also have special newline handling in HTML. + * * @ticket 64609 * * @dataProvider data_modifiable_text_special_textarea @@ -51,15 +55,15 @@ public function test_modifiable_text_special_textarea( string $set_text, string */ public static function data_modifiable_text_special_textarea() { return array( - 'Leading newline' => array( + 'Leading newline' => array( "\nAFTER NEWLINE", "", ), - 'Leading carriage return' => array( + 'Leading carriage return' => array( "\rCR", "", ), - 'Leading carriage return + newline' => array( + 'Leading carriage return + newline' => array( "\r\nCR-N", "", ), @@ -68,56 +72,14 @@ public static function data_modifiable_text_special_textarea() { /** * PRE and LISTING elements ignore the first newline in their content. - * Setting the modifiable text with a leading newline should ensure that the leading newline - * is present in the resulting element. - * - * @ticket 64609 - * - * @dataProvider data_modifiable_text_special_pre_tags - * - * @param string $tag_name The tag name to test (e.g. 'pre', 'listing'). - */ - public function test_modifiable_text_special_pre_tags( string $tag_name ) { - $set_text = "\nAFTER NEWLINE"; - $processor = WP_HTML_Processor::create_fragment( "<{$tag_name}>REPLACEME" ); - $processor->next_tag(); - $processor->next_token(); - $this->assertSame( '#text', $processor->get_token_type() ); - $processor->set_modifiable_text( $set_text ); - $this->assertSame( $set_text, $processor->get_modifiable_text() ); - $this->assertEqualHTML( - << - {$set_text} - HTML, - $processor->get_updated_html(), - '', - "Should have preserved the leading newline in the {$tag_name} content." - ); - } - - /** - * Data provider. + * Leading whitespace may split into multiple text nodes in the HTML Processor. + * Setting the modifiable text with a leading newline should ensure that the + * leading newline is present in the resulting element. * - * @return array[] - */ - public static function data_modifiable_text_special_pre_tags() { - return array( - 'PRE' => array( 'pre' ), - 'LISTING' => array( 'listing' ), - ); - } - - /** * The HTML Processor has special behavior when a text node starts with whitespace. * Test that PRE and LISTING `::set_modifiable_text()` handling works correctly * with leading whitespace. * - * PRE and LISTING elements ignore the first newline in their content. - * Leading whitespace may split into multiple text nodes in the HTML Processor. - * Setting the modifiable text with a leading newline should ensure that the - * leading newline is present in the resulting element. - * * @ticket 64609 * * @dataProvider data_modifiable_text_special_leading_whitespace @@ -167,45 +129,49 @@ public function test_modifiable_text_special_leading_whitespace( * Data provider. */ public static function data_modifiable_text_special_leading_whitespace() { - $set_text = "\nAFTER NEWLINE."; - - foreach ( self::data_modifiable_text_special_pre_tags() as $tag_data ) { - $tag_name = $tag_data[0]; - $tag_label = strtoupper( $tag_name ); + $tags = array( 'pre', 'listing' ); + foreach ( $tags as $tag_name ) { + yield "<{$tag_name}> with no leading newline" => array( + "<{$tag_name}>REPLACEME", + 1, + "REPLACEME", + "\nAFTER NEWLINE.", + "<{$tag_name}>\n\nAFTER NEWLINE.", + ); - yield "{$tag_label} with leading newline, first text node" => array( + yield "<{$tag_name}> with leading newline, first text node" => array( "<{$tag_name}>\nREPLACEME", 1, '', - $set_text, - "<{$tag_name}>\n{$set_text}REPLACEME", + "\nAFTER NEWLINE.", + "<{$tag_name}>\n\nAFTER NEWLINE.REPLACEME", ); - yield "{$tag_label} with leading newline, second text node" => array( + yield "<{$tag_name}> with leading newline, second text node" => array( "<{$tag_name}>\nREPLACEME", 2, 'REPLACEME', - $set_text, - "<{$tag_name}>\n{$set_text}", + "\nAFTER NEWLINE.", + "<{$tag_name}>\n\nAFTER NEWLINE.", ); - yield "{$tag_label} with leading space, first text node" => array( + yield "<{$tag_name}> with leading space, first text node" => array( "<{$tag_name}> REPLACEME", 1, ' ', - $set_text, - "<{$tag_name}>\n{$set_text}REPLACEME", + "\nAFTER NEWLINE.", + "<{$tag_name}>\n\nAFTER NEWLINE.REPLACEME", ); - yield "{$tag_label} with leading space, second text node" => array( + yield "<{$tag_name}> with leading space, second text node" => array( "<{$tag_name}> REPLACEME", 2, 'REPLACEME', - $set_text, - "<{$tag_name}>\n {$set_text}", + "\nAFTER NEWLINE.", + "<{$tag_name}>\n \nAFTER NEWLINE.", ); - yield "{$tag_label} insert with leading carriage return" => array( + yield "<{$tag_name}> insert with leading carriage return" => array( "<{$tag_name}>REPLACEME", 1, 'REPLACEME', @@ -213,7 +179,7 @@ public static function data_modifiable_text_special_leading_whitespace() { "<{$tag_name}>\n\nCR", ); - yield "{$tag_label} insert with leading carriage return + newline" => array( + yield "<{$tag_name}> insert with leading carriage return + newline" => array( "<{$tag_name}>REPLACEME", 1, 'REPLACEME', @@ -221,7 +187,7 @@ public static function data_modifiable_text_special_leading_whitespace() { "<{$tag_name}>\n\nCR-N", ); - yield "{$tag_label} clear text" => array( + yield "<{$tag_name}> clear text" => array( "<{$tag_name}>REPLACEME", 1, 'REPLACEME', From ee98623b02e841f664336a32aa91c6972b641cd5 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 15:15:46 +0100 Subject: [PATCH 17/18] Lint --- tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php index d7f6e33785224..5a4bcfe606459 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php @@ -134,7 +134,7 @@ public static function data_modifiable_text_special_leading_whitespace() { yield "<{$tag_name}> with no leading newline" => array( "<{$tag_name}>REPLACEME", 1, - "REPLACEME", + 'REPLACEME', "\nAFTER NEWLINE.", "<{$tag_name}>\n\nAFTER NEWLINE.", ); From 8340285323b7afee7e4e452b3a5e9021cdac55ef Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 6 Feb 2026 15:34:44 +0100 Subject: [PATCH 18/18] Improve explanatory comments --- .../html-api/class-wp-html-tag-processor.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index ee6f6ef9b9e1c..7843a83b3932c 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3771,10 +3771,8 @@ public function get_modifiable_text(): string { public function set_modifiable_text( string $plaintext_content ): bool { if ( self::STATE_TEXT_NODE === $this->parser_state ) { /* - * In case the text starts at a position where a newline is skipped _and_ starts - * with a newline, add an additional newline. - * This preserves the intention of adding text with a leading newline which would - * be removed in HTML. + * HTML ignores a single leading newline in this context. If a leading newline + * is intended, preserve it by adding an extra newline. */ if ( $this->skip_newline_at === $this->text_starts_at && @@ -3889,10 +3887,10 @@ static function ( $tag_match ) { }, $plaintext_content ); + /* - * A single leading newline will be removed from TEXTAREA contents, if present. - * If the intention is to start with a leading newline, ensure that it is preserved - * by adding an additional leading newline. + * HTML ignores a single leading newline in this context. If a leading newline + * is intended, preserve it by adding an extra newline. */ if ( 'TEXTAREA' === $this->get_tag() &&