Commit 71af89d97b for wordpress.org

commit 71af89d97b375fe608c2b25f88d86a5acde2c002
Author: dmsnell <dmsnell@git.wordpress.org>
Date:   Wed Oct 28 23:52:57 2026 +0000

    Blocks: Fix implicit block closing in default block parser.

    When reaching the end of a document during block parsing, and blocks remain open, existing open blocks should implicitly close. Previously, there was a mistake in the closing code which duplicated content in the implicitly-closed blocks.

    In this patch, that implicit closing of open blocks is moved into a new helper method where the logic can be more clearly isolated and analyzed, and the defect is resolved.

    Developed in: https://github.com/WordPress/wordpress-develop/pull/13720
    Discussed in: https://core.trac.wordpress.org/ticket/66138

    Props dmsnell, jonsurrell, westonruter.
    See #66138.

    Built from https://develop.svn.wordpress.org/trunk@64996


    git-svn-id: http://core.svn.wordpress.org/trunk@64146 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/class-wp-block-parser.php b/wp-includes/class-wp-block-parser.php
index 03b9b1b3db..1689dafcd2 100644
--- a/wp-includes/class-wp-block-parser.php
+++ b/wp-includes/class-wp-block-parser.php
@@ -109,35 +109,32 @@ class WP_Block_Parser {

 		// Handle the cases when no more tokens exist.
 		if ( ! $processor->next_delimiter() ) {
-			// if not in a block then flush output.
+			/*
+			 * Most documents will end with all blocks properly closed.
+			 * In these cases, all that’s necessary is to flush the output.
+			 * There is probably a terminating newline.
+			 */
 			if ( 0 === $stack_depth ) {
 				$this->add_freeform();
 				return false;
 			}

 			/*
-			 * Otherwise we have a problem
-			 * This is an error
+			 * However, if blocks remain open it means there were missing
+			 * closing block delimiters. The post may have been truncated,
+			 * for example, but some amount of corruption produced a malformed
+			 * document.
 			 *
-			 * we have options
-			 * - treat it all as freeform text
-			 * - assume an implicit closer (easiest when not nesting)
-			 */
-
-			// for the easy case we'll assume an implicit closer.
-			if ( 1 === $stack_depth ) {
-				$this->add_block_from_stack();
-				return false;
-			}
-
-			/*
-			 * for the nested case where it's more difficult we'll
-			 * have to assume that multiple closers are missing
-			 * and so we'll collapse the whole stack piecewise
+			 * The spec parser considers this a parse error and returns a long
+			 * freeform block containing the full span of text from the input
+			 * document from the start of the un-closed block.
+			 *
+			 * This parser, however, is making a different pragmatic choice: it
+			 * will implicitly close any remaining open blocks, as if the closers
+			 * were provided. This ensures recovery of as much of the provided
+			 * block structure as is possible.
 			 */
-			while ( 0 < count( $this->stack ) ) {
-				$this->add_block_from_stack();
-			}
+			$this->implicitly_close_and_add_all_open_blocks();
 			return false;
 		}

@@ -225,11 +222,14 @@ class WP_Block_Parser {
 				 * otherwise we're nested and we have to close out the current
 				 * block and add it as a new innerBlock to the parent
 				 */
-				$stack_top                        = array_pop( $this->stack );
-				$html                             = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
-				$stack_top->block->innerHTML     .= $html;
-				$stack_top->block->innerContent[] = $html;
-				$stack_top->prev_offset           = $start_offset + $token_length;
+				$stack_top              = array_pop( $this->stack );
+				$html                   = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
+				$stack_top->prev_offset = $start_offset + $token_length;
+
+				if ( '' !== $html ) {
+					$stack_top->block->innerHTML     .= $html;
+					$stack_top->block->innerContent[] = $html;
+				}

 				$this->add_inner_block(
 					$stack_top->block,
@@ -345,7 +345,7 @@ class WP_Block_Parser {
 		$parent->block->innerBlocks[] = (array) $block;
 		$html                         = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset );

-		if ( ! empty( $html ) ) {
+		if ( '' !== $html ) {
 			$parent->block->innerHTML     .= $html;
 			$parent->block->innerContent[] = $html;
 		}
@@ -370,7 +370,7 @@ class WP_Block_Parser {
 			? substr( $this->document, $prev_offset, $end_offset - $prev_offset )
 			: substr( $this->document, $prev_offset );

-		if ( ! empty( $html ) ) {
+		if ( '' !== $html ) {
 			$stack_top->block->innerHTML     .= $html;
 			$stack_top->block->innerContent[] = $html;
 		}
@@ -387,6 +387,49 @@ class WP_Block_Parser {

 		$this->output[] = (array) $stack_top->block;
 	}
+
+	/**
+	 * Closes all open blocks and adds remaining blocks and freeform content to the output list.
+	 *
+	 * @internal
+	 * @since 7.2.0
+	 */
+	public function implicitly_close_and_add_all_open_blocks(): void {
+		$implicitly_closed = null;
+		$last_freeform     = null;
+
+		while ( null !== ( $stack_top = array_pop( $this->stack ) ) ) {
+			if ( isset( $last_freeform ) ) {
+				$html = substr( $this->document, $stack_top->prev_offset, $last_freeform[1] - $stack_top->prev_offset );
+			} else {
+				$html = substr( $this->document, $stack_top->prev_offset );
+			}
+
+			if ( '' !== $html ) {
+				$stack_top->block->innerHTML     .= $html;
+				$stack_top->block->innerContent[] = $html;
+			}
+
+			// Trap potential leading freeform content for the final output.
+			$last_freeform = array( $stack_top->leading_html_start ?? 0, $stack_top->token_start );
+
+			if ( isset( $implicitly_closed ) ) {
+				$stack_top->block->innerContent[] = null;
+				$stack_top->block->innerBlocks[]  = $implicitly_closed;
+			}
+
+			$implicitly_closed = $stack_top->block;
+		}
+
+		if ( isset( $last_freeform ) && $last_freeform[1] > $last_freeform[0] ) {
+			$html           = substr( $this->document, $last_freeform[0], $last_freeform[1] - $last_freeform[0] );
+			$this->output[] = (array) $this->freeform( $html );
+		}
+
+		if ( isset( $implicitly_closed ) ) {
+			$this->output[] = (array) $implicitly_closed;
+		}
+	}
 }

 /**
diff --git a/wp-includes/class-wp-block-processor.php b/wp-includes/class-wp-block-processor.php
index 7ad4de1f2d..0c6f9dcee1 100644
--- a/wp-includes/class-wp-block-processor.php
+++ b/wp-includes/class-wp-block-processor.php
@@ -1277,20 +1277,16 @@ class WP_Block_Processor {
 	 *
 	 * @since 6.9.0
 	 *
-	 * @return array[]|null {
-	 *     Array of block structures.
-	 *
-	 *     @type array ...$0 {
-	 *         An associative array of a single parsed block object. See WP_Block_Parser_Block.
-	 *
-	 *         @type string|null $blockName    Name of block.
-	 *         @type array       $attrs        Attributes from block comment delimiters.
-	 *         @type array[]     $innerBlocks  List of inner blocks. An array of arrays that
-	 *                                         have the same structure as this one.
-	 *         @type string      $innerHTML    HTML from inside block comment delimiters.
-	 *         @type array       $innerContent List of string fragments and null markers where
-	 *                                         inner blocks were found.
-	 *     }
+	 * @return array|null {
+	 *     An associative array of a single parsed block object. See WP_Block_Parser_Block.
+	 *
+	 *     @type string|null $blockName    Name of block.
+	 *     @type array       $attrs        Attributes from block comment delimiters.
+	 *     @type array[]     $innerBlocks  List of inner blocks. An array of arrays that
+	 *                                     have the same structure as this one.
+	 *     @type string      $innerHTML    HTML from inside block comment delimiters.
+	 *     @type array       $innerContent List of string fragments and null markers where
+	 *                                     inner blocks were found.
 	 * }
 	 */
 	public function extract_full_block_and_advance(): ?array {
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 11b659bd7e..552c849c3c 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta3-64995';
+$wp_version = '7.2-beta3-64996';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.