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.