Skip to content
54 changes: 54 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,60 @@ function shortcode_unautop( $text ) {
return preg_replace( $pattern, '$1', $text );
}

/**
* Strips unnecessary newlines from HTML content.
*
* @since 7.1.0
*
* @param string $text The HTML content to strip newlines from.
* @return string The HTML content with unnecessary newlines removed.
*/
function strip_html_newlines( $text ) {
if ( ! str_contains( $text, "\n" ) && ! str_contains( $text, "\r" ) ) {
return $text;
}

$preserve_newlines_elements = array( 'pre', 'code', 'kbd', 'script', 'style' );

$textarr = wp_html_split( $text );
$changed = false;
$skip = false;

foreach ( $textarr as $i => $chunk ) {
if ( '' === $chunk ) {
continue;
}

if ( 0 !== $i % 2 ) {
foreach ( $preserve_newlines_elements as $element ) {
if ( stripos( $chunk, '<' . $element ) === 0 ) {
$skip = true;
break;
}
if ( stripos( $chunk, '</' . $element . '>' ) === 0 ) {
$skip = false;
break;
}
}
continue;
}

if ( ! $skip ) {
$stripped = preg_replace( '/[\n\r]+/', ' ', $chunk );
if ( $stripped !== $chunk ) {
$textarr[ $i ] = $stripped;
$changed = true;
}
}
}

if ( $changed ) {
return implode( '', $textarr );
}

return $text;
}

/**
* Checks to see if a string is utf8 encoded.
*
Expand Down
54 changes: 54 additions & 0 deletions tests/phpunit/tests/formatting/stripHtmlNewlines.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/**
* Tests for strip_html_newlines().
*
* @group formatting
*
* @covers ::strip_html_newlines
*/
class Tests_Formatting_StripHtmlNewlines extends WP_UnitTestCase {

/**
* Verifies that newlines and carriage returns in text nodes are replaced
* with spaces, including across inline elements like anchors.
*
* @ticket 5678
*/
public function test_strips_newlines_from_text_nodes() {
$this->assertSame( '', strip_html_newlines( '' ), 'Empty string should be returned as-is.' );
$this->assertSame( '<p>No newlines here.</p>', strip_html_newlines( '<p>No newlines here.</p>' ), 'Text without newlines should be returned as-is.' );
$this->assertSame( '<p>Line one Line two Line three</p>', strip_html_newlines( "<p>Line one\n\nLine two\r\nLine three</p>" ), 'Multiple newlines and carriage returns should be collapsed to a single space.' );
$this->assertSame(
'<p>This is a paragraph in which the wpautop() <a href="#elsewhere">wrapping will happen in the middle</a> of an anchor, which is an inline element.</p>',
strip_html_newlines( "<p>This is a paragraph in which the\nwpautop() <a href=\"#elsewhere\">wrapping will\nhappen in the middle</a> of an\nanchor, which is an inline element.</p>" ),
'Newlines within and around inline elements should be stripped.'
);
}

/**
*
* @ticket 5678
*/
public function test_preserves_newlines_in_preformatted_elements() {
$input = "<p>Normal\ntext</p>\n<pre>\nPreformatted\nlines\n</pre>\n<p>More\ntext</p>";
$result = strip_html_newlines( $input );

$this->assertStringContainsString( 'Normal text', $result, 'Newlines in normal text should be stripped.' );
$this->assertStringContainsString( 'More text', $result, 'Newlines in trailing paragraph should be stripped.' );
$this->assertStringContainsString( "\nPreformatted\nlines\n", $result, 'Newlines inside <pre> should be preserved.' );

$preserved_cases = array(
'code' => "<p>A\nB</p><code>x\ny</code>",
'kbd' => "<p>A\nB</p><kbd>x\ny</kbd>",
'script' => "<p>A\nB</p><script>x\ny</script>",
'style' => "<p>A\nB</p><style>x\ny</style>",
);

foreach ( $preserved_cases as $tag => $html ) {
$out = strip_html_newlines( $html );
$this->assertStringContainsString( 'A B', $out, "Text node newline should be stripped around <{$tag}>." );
$this->assertStringContainsString( "x\ny", $out, "Newline inside <{$tag}> should be preserved." );
}
}
}
Loading