From 9e853c0e88885faa1c9d0f4eb1163a12413fc27b Mon Sep 17 00:00:00 2001 From: VladimirAus Date: Thu, 29 Jan 2026 23:46:14 +1000 Subject: [PATCH 1/3] [#38656] Applied patch #4 with updated tests. --- src/wp-includes/formatting.php | 81 +++++++++++++++++++++- tests/phpunit/tests/formatting/wpAutop.php | 52 ++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index f59f877775b77..de3fce5dfb817 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -484,7 +484,73 @@ function wpautop( $text, $br = true ) { // Change multiple
's into two line breaks, which will turn into paragraphs. $text = preg_replace( '|\s*|', "\n\n", $text ); - $allblocks = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)'; + // Block level elements that can contain a

. + $peeblocks = 'caption|td|th|div|dd|dt|li|form|map|blockquote|address|fieldset|section|article|aside|header|footer|nav|figure|figcaption|details|menu'; + + // Block level elements that cannot contain a

. + $peefreeblocks = 'table|thead|tfoot|col|colgroup|tbody|tr|dl|ul|ol|pre|area|math|style|p|h[1-6]|hr|legend|hgroup|summary'; + + $allblocks = "(?:$peeblocks|$peefreeblocks)"; + $peeblocks = "(?:$peeblocks)"; + $peefreeblocks = "(?:$peefreeblocks)"; + + // Empty elements that cannot contain anything. + $emptyblocks = '(?:hr|br)'; + + // Media elements that will be processed later. + $mediablocks = '(?:object|param|embed|audio|video|source|track)'; + + // Split the HTML into a stream of elements and text nodes. + $stream = wp_html_split( $text ); + + // Assume that the top level is pee-able. + $peeable = array( true ); + + foreach ( $stream as $id => $droplet ) { + if ( '<' === substr( $droplet, 0, 1 ) ) { + if (preg_match('!<' . $emptyblocks . '(?: [^>]*)?>!s', $droplet)) { + // If we encounter an empty element, ignore it. + continue; + } elseif (preg_match('!^<.+/>$!s', $droplet)) { + // Ignore self-closing elements, too. + continue; + } elseif (preg_match('%^$%s', $droplet)) { + // Comments can be ignored. + continue; + } elseif (preg_match('%^$%s', $droplet)) { + // CDATA can totally be ignored. + continue; + } elseif (preg_match('!^$!s', $droplet)) { + // Here's closing element, let's move back up a level. + array_pop($peeable); + } elseif (preg_match('!^<' . $peeblocks . '(?: [^>]*)?>$!s', $droplet)) { + // We've just entered a pee-able block, mark it so. + $peeable[] = true; + } elseif (preg_match('!^<' . $mediablocks . '(?: [^>]*)?>$!s', $droplet)) { + // Media blocks can't really be pee'd, but they are handled later on. + $peeable[] = true; + } else { + $peeable[] = false; + } + + // We can't pee an element, so move to the next droplet. + continue; + + } + + // If the current level can't be pee'd, protect it from being pee'd later. + if ( ! end( $peeable ) ) { + $stream[ $id ] = str_replace( "\n", '', $droplet ); + } + } + + // If we accidentally marked the final newline as being unpee-able (for + // example, if there's some malformed HTML that didn't close properly), fix it. + if ( '' === $stream[ $id ] ) { + $stream[ $id ] = "\n"; + } + + $text = implode( $stream ); // Add a double line break above block-level opening tags. $text = preg_replace( '!(<' . $allblocks . '[\s/>])!', "\n\n$1", $text ); @@ -492,6 +558,12 @@ function wpautop( $text, $br = true ) { // Add a double line break below block-level closing tags. $text = preg_replace( '!()!', "$1\n\n", $text ); + // Add a double line break below block-level opening tags that are allowed to contain a

. + $text = preg_replace( '%(<' . $peeblocks . '(?: [^>]*)?>)%', "$1\n\n", $text ); + + // Add a double line break above block-level closing tags that are allowed to contain a

. + $text = preg_replace( '%()%', "\n\n$1", $text ); + // Add a double line break after hr tags, which are self closing. $text = preg_replace( '!()!', "$1\n\n", $text ); @@ -533,6 +605,9 @@ function wpautop( $text, $br = true ) { $text = preg_replace( '|\s*|', '', $text ); } + // If there's only one paragraph inside a pee block, remove the newlines. + $text = preg_replace( '%(<(' . $peeblocks . ")(?: [^>]*)?>)\n\n((?(?!\n\n).)*)\n\n()%s", '$1$3$4', $text ); + // Remove more than two contiguous line breaks. $text = preg_replace( "/\n\n+/", "\n\n", $text ); @@ -564,10 +639,10 @@ function wpautop( $text, $br = true ) { $text = str_replace( '

', '

', $text ); // If an opening or closing block element tag is preceded by an opening

tag, remove it. - $text = preg_replace( '!

\s*(]*>)!', '$1', $text ); + $text = preg_replace( '%

(?:\s|)*(]*>)%', '$1', $text ); // If an opening or closing block element tag is followed by a closing

tag, remove it. - $text = preg_replace( '!(]*>)\s*

!', '$1', $text ); + $text = preg_replace( '%(]*>)(?:\s|)*

%', '$1', $text ); // Optionally insert line breaks. if ( $br ) { diff --git a/tests/phpunit/tests/formatting/wpAutop.php b/tests/phpunit/tests/formatting/wpAutop.php index ee4d90645d09c..d0b472aa8cb01 100644 --- a/tests/phpunit/tests/formatting/wpAutop.php +++ b/tests/phpunit/tests/formatting/wpAutop.php @@ -392,6 +392,9 @@ public function test_that_wpautop_treats_block_level_elements_as_blocks() { $content = array(); foreach ( $blocks as $block ) { + if ( 'hr' === $block ) { + continue; + } $content[] = "<$block>foo"; } @@ -420,6 +423,9 @@ public function test_that_wpautop_treats_block_level_elements_as_blocks() { $content = array(); foreach ( $blocks as $block ) { + if ( 'hr' === $block ) { + continue; + } $content[] = "<$block attr='value'>foo"; } @@ -660,4 +666,50 @@ public function test_that_wpautop_ignores_inline_scripts() { $this->assertSameIgnoreEOL( $expected, trim( wpautop( $content ) ) ); } + + /** + * Data provider for test_paragraphs_inside_blocks(). + * + * @ticket 38656 + */ + public function data_paragraphs_inside_blocks() { + $data = array( + array( + "
a\n\nb
", + "
\n

a

\n

b

\n
", + ), + array( + "

a\n\nb

", + "

a\n\nb

", + ), + array( + "
  • a\n\nb
", + "
    \n
  • \n

    a

    \n

    b

    \n
  • \n
", + ), + array( + "
  • a\n\n
    • b\n\nc
", + "
    \n
  • \n

    a

    \n
      \n
    • \n

      b

      \n

      c

      \n
    • \n
    \n
  • \n
", + ), + array( + "
a\n\n
\n\nb
", + "
\n

a

\n
\n

b

\n
", + ), + array( + "\n\n\n\n\n\n\n\n
a\n\nb\n\n
  • c\n\n
    • d\n\ne
    f\n\ng
h
", + "\n\n\n\n\n
\n

a

\n

b

\n
\n
    \n
  • \n

    c

    \n
      \n
    • \n

      d

      \n

      e

      \n
    • \n
    \n

    f

    \n

    g

    \n
  • \n
\n

h

\n
", + ), + ); + + return $data; + } + + /** + * wpautop() incorrectly handling paragraphs within block elements + * + * @ticket 38656 + * @dataProvider data_paragraphs_inside_blocks + */ + public function test_paragraphs_inside_blocks( $input, $output ) { + return $this->assertSame( $output, wpautop( $input ) ); + } } From 6341380164fca774d5585e7eada82bf20cee2690 Mon Sep 17 00:00:00 2001 From: VladimirAus Date: Thu, 29 Jan 2026 23:53:51 +1000 Subject: [PATCH 2/3] [#38656] PHPcs fixes. --- src/wp-includes/formatting.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index de3fce5dfb817..749a85f6549dd 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -508,25 +508,25 @@ function wpautop( $text, $br = true ) { foreach ( $stream as $id => $droplet ) { if ( '<' === substr( $droplet, 0, 1 ) ) { - if (preg_match('!<' . $emptyblocks . '(?: [^>]*)?>!s', $droplet)) { + if ( preg_match( '!<' . $emptyblocks . '(?: [^>]*)?>!s', $droplet ) ) { // If we encounter an empty element, ignore it. continue; - } elseif (preg_match('!^<.+/>$!s', $droplet)) { + } elseif ( preg_match( '!^<.+/>$!s', $droplet ) ) { // Ignore self-closing elements, too. continue; - } elseif (preg_match('%^$%s', $droplet)) { + } elseif ( preg_match( '%^$%s', $droplet ) ) { // Comments can be ignored. continue; - } elseif (preg_match('%^$%s', $droplet)) { + } elseif ( preg_match( '%^$%s', $droplet ) ) { // CDATA can totally be ignored. continue; - } elseif (preg_match('!^$!s', $droplet)) { + } elseif ( preg_match( '!^$!s', $droplet ) ) { // Here's closing element, let's move back up a level. array_pop($peeable); - } elseif (preg_match('!^<' . $peeblocks . '(?: [^>]*)?>$!s', $droplet)) { + } elseif ( preg_match( '!^<' . $peeblocks . '(?: [^>]*)?>$!s', $droplet ) ) { // We've just entered a pee-able block, mark it so. $peeable[] = true; - } elseif (preg_match('!^<' . $mediablocks . '(?: [^>]*)?>$!s', $droplet)) { + } elseif ( preg_match( '!^<' . $mediablocks . '(?: [^>]*)?>$!s', $droplet ) ) { // Media blocks can't really be pee'd, but they are handled later on. $peeable[] = true; } else { From 4a693ae9f2bead69d06dbdcf77832e991c09a9b6 Mon Sep 17 00:00:00 2001 From: VladimirAus Date: Thu, 29 Jan 2026 23:56:30 +1000 Subject: [PATCH 3/3] [#38656] PHPcs fixes. --- src/wp-includes/formatting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 749a85f6549dd..5fcc3cb99f4f5 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -522,7 +522,7 @@ function wpautop( $text, $br = true ) { continue; } elseif ( preg_match( '!^$!s', $droplet ) ) { // Here's closing element, let's move back up a level. - array_pop($peeable); + array_pop( $peeable ); } elseif ( preg_match( '!^<' . $peeblocks . '(?: [^>]*)?>$!s', $droplet ) ) { // We've just entered a pee-able block, mark it so. $peeable[] = true;