From c814a978d6847a7cdbebe3816edec36750480cd2 Mon Sep 17 00:00:00 2001
From: ArkaPrabhaChowdhury
Date: Tue, 14 Jul 2026 12:37:30 +0530
Subject: [PATCH] Formatting: Prevent wpautop() from splitting anchors around
block elements
---
src/wp-includes/formatting.php | 39 +++++++++++++++++++++-
tests/phpunit/tests/formatting/wpAutop.php | 15 +++++++++
2 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index acacb131a627b..a718b4d5f4389 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -444,7 +444,9 @@ function _wptexturize_pushpop_element( $text, &$stack, $disabled_elements ) {
* @return string Text which has been converted into correct paragraph tags.
*/
function wpautop( $text, $br = true ) {
- $pre_tags = array();
+ $pre_tags = array();
+ $has_block_anchor = false;
+ $block_anchor_placeholder = '';
if ( '' === trim( $text ) ) {
return '';
@@ -486,6 +488,29 @@ function wpautop( $text, $br = true ) {
$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)';
+ /*
+ * Anchors can contain block-level elements. Replace their tags with temporary
+ * block-level placeholders so the paragraph cleanup does not split the anchor.
+ */
+ if ( str_contains( $text, ']*)>(?=\s*<' . $allblocks . '[\s/>])(.*?)()~s',
+ '<' . $block_anchor_placeholder . '$1>$2' . $block_anchor_placeholder . '>',
+ $text
+ );
+ $has_block_anchor = str_contains( $text, '<' . $block_anchor_placeholder );
+
+ if ( $has_block_anchor ) {
+ $allblocks = substr( $allblocks, 0, -1 ) . '|' . $block_anchor_placeholder . ')';
+ }
+ }
+
// Add a double line break above block-level opening tags.
$text = preg_replace( '!(<' . $allblocks . '[\s/>])!', "\n\n$1", $text );
@@ -563,6 +588,13 @@ function wpautop( $text, $br = true ) {
$text = preg_replace( '|]*)>|i', '', $text );
$text = str_replace( '
', '', $text );
+ // Move paragraphs inside anchors that contain block-level elements.
+ if ( $has_block_anchor ) {
+ $text = preg_replace( '|(<' . $block_anchor_placeholder . '[^>]*>)|', '$1
', $text );
+ $text = preg_replace( '|(' . $block_anchor_placeholder . '>)
|', '$1', $text );
+ $text = preg_replace( '|\s*
|', '', $text );
+ }
+
// If an opening or closing block element tag is preceded by an opening tag, remove it.
$text = preg_replace( '!
\s*(?' . $allblocks . '[^>]*>)!', '$1', $text );
@@ -601,6 +633,11 @@ function wpautop( $text, $br = true ) {
$text = str_replace( array( ' ', '' ), "\n", $text );
}
+ // Restore anchors that contain block-level elements.
+ if ( $has_block_anchor ) {
+ $text = str_replace( array( '<' . $block_anchor_placeholder, '' . $block_anchor_placeholder . '>' ), array( '' ), $text );
+ }
+
return $text;
}
diff --git a/tests/phpunit/tests/formatting/wpAutop.php b/tests/phpunit/tests/formatting/wpAutop.php
index ee4d90645d09c..c7ebe1fbf9907 100644
--- a/tests/phpunit/tests/formatting/wpAutop.php
+++ b/tests/phpunit/tests/formatting/wpAutop.php
@@ -490,6 +490,21 @@ public function test_that_wpautop_treats_inline_elements_as_inline() {
$this->assertSame( $expected, trim( wpautop( $content ) ) );
}
+ /**
+ * @ticket 40202
+ */
+ public function test_that_wpautop_does_not_split_anchors_containing_block_elements() {
+ $content = 'Text
';
+ $expected = "\nText
\n";
+
+ $this->assertSame( $expected, trim( wpautop( $content ) ) );
+
+ $content = 'First
';
+ $expected = "\nFirst
\n\n";
+
+ $this->assertSame( $expected, trim( wpautop( $content ) ) );
+ }
+
/**
* Do not allow newlines within HTML elements to become mangled.
*