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
81 changes: 78 additions & 3 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -484,14 +484,86 @@ function wpautop( $text, $br = true ) {
// Change multiple <br>'s into two line breaks, which will turn into paragraphs.
$text = preg_replace( '|<br\s*/?>\s*<br\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 <p>.
$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 <p>.
$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( '%^<!\[CDATA\[.+\]\]>$%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", '<!-- wpnl -->', $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 ( '<!-- wpnl -->' === $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 );

// Add a double line break below block-level closing tags.
$text = preg_replace( '!(</' . $allblocks . '>)!', "$1\n\n", $text );

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

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

// Add a double line break after hr tags, which are self closing.
$text = preg_replace( '!(<hr\s*?/?>)!', "$1\n\n", $text );

Expand Down Expand Up @@ -533,6 +605,9 @@ function wpautop( $text, $br = true ) {
$text = preg_replace( '|</figcaption>\s*|', '</figcaption>', $text );
}

// If there's only one paragraph inside a pee block, remove the newlines.
$text = preg_replace( '%(<(' . $peeblocks . ")(?: [^>]*)?>)\n\n((?(?!\n\n).)*)\n\n(</\\2>)%s", '$1$3$4', $text );

// Remove more than two contiguous line breaks.
$text = preg_replace( "/\n\n+/", "\n\n", $text );

Expand Down Expand Up @@ -564,10 +639,10 @@ function wpautop( $text, $br = true ) {
$text = str_replace( '</blockquote></p>', '</p></blockquote>', $text );

// If an opening or closing block element tag is preceded by an opening <p> tag, remove it.
$text = preg_replace( '!<p>\s*(</?' . $allblocks . '[^>]*>)!', '$1', $text );
$text = preg_replace( '%<p>(?:\s|<!-- wpnl -->)*(</?' . $allblocks . '[^>]*>)%', '$1', $text );

// If an opening or closing block element tag is followed by a closing <p> tag, remove it.
$text = preg_replace( '!(</?' . $allblocks . '[^>]*>)\s*</p>!', '$1', $text );
$text = preg_replace( '%(</?' . $allblocks . '[^>]*>)(?:\s|<!-- wpnl -->)*</p>%', '$1', $text );

// Optionally insert line breaks.
if ( $br ) {
Expand Down
52 changes: 52 additions & 0 deletions tests/phpunit/tests/formatting/wpAutop.php
Original file line number Diff line number Diff line change
Expand Up @@ -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</$block>";
}

Expand Down Expand Up @@ -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</$block>";
}

Expand Down Expand Up @@ -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(
"<div>a\n\nb</div>",
"<div>\n<p>a</p>\n<p>b</p>\n</div>",
),
array(
"<h1>a\n\nb</h1>",
"<h1>a\n\nb</h1>",
),
array(
"<ul><li>a\n\nb</li></ul>",
"<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>",
),
array(
"<ul><li>a\n\n<ul><li>b\n\nc</li></ul></li></ul>",
"<ul>\n<li>\n<p>a</p>\n<ul>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n</ul>\n</li>\n</ul>",
),
array(
"<div>a\n\n<hr>\n\nb</div>",
"<div>\n<p>a</p>\n<hr>\n<p>b</p>\n</div>",
),
array(
"<table>\n\n<tr>\n\n<td>a\n\nb</td><td>\n\n<ul><li>c\n\n<ul><li>d\n\ne</li></ul>f\n\ng</li></ul>h</td>\n\n</tr>\n\n</table>",
"<table>\n<tr>\n<td>\n<p>a</p>\n<p>b</p>\n</td>\n<td>\n<ul>\n<li>\n<p>c</p>\n<ul>\n<li>\n<p>d</p>\n<p>e</p>\n</li>\n</ul>\n<p>f</p>\n<p>g</p>\n</li>\n</ul>\n<p>h</p>\n</td>\n</tr>\n</table>",
),
);

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 ) );
}
}
Loading