<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56649] trunk: Blocks: Implement automatic block insertion into Block Hooks.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56649">56649</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56649","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>Bernhard Reiter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-09-21 16:16:05 +0000 (Thu, 21 Sep 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Blocks: Implement automatic block insertion into Block Hooks.

Block Hooks allow a third-party block to specify a position relative to a given block into which it will then be automatically inserted (e.g. a "Like" button block can ask to be inserted after the Post Content block, or an eCommerce shopping cart block can ask to be inserted after the Navigation block).

The underlying idea is to provide an extensibility mechanism for Block Themes, in analogy to WordPress' [https://developer.wordpress.org/plugins/hooks/ Hooks] concept that has allowed extending Classic Themes through filters and actions.

The two core tenets for Block Hooks are:

1. Insertion into the frontend should happen right after a plugin containing a hooked block is activated (i.e. the user isn't required to insert the block manually in the editor first); similarly, disabling the plugin should remove the hooked block from the frontend.
2. The user has the ultimate power to customize that automatic insertion: The hooked block is also visible in the editor, and the user's decision to persist, dismiss (i.e. remove), customize, or move it will be respected (and reflected on the frontend).

To account for both tenets, the **tradeoff** was made to limit automatic block insertion to unmodified templates (and template parts, respectively). The reason for this is that the simplest way of storing the information whether a block has been persisted to (or dismissed from) a given template (or part) is right in the template markup.

To accommodate for that tradeoff, [https://github.com/WordPress/gutenberg/pull/52969 UI controls (toggles)] are being added to increase visibility of hooked blocks, and to allow for their later insertion into templates (or parts) that already have been modified by the user.

For hooked blocks to appear both in the frontend and in the editor (see tenet number 2), they need to be inserted into both the frontend markup and the REST API (templates and patterns endpoints) equally. As a consequence, this means that automatic insertion couldn't (only) be implemented at block ''render'' stage, as for the editor, the ''serialized'' (but ''unrendered'') markup needs to be modified.

Furthermore, hooked blocks also have to be inserted into block patterns. Since practically no filters exist for the patterns registry, this has to be done in the registry's `get_registered` and `get_all_registered` methods.

Props gziolo.
Fixes <a href="https://core.trac.wordpress.org/ticket/59313">#59313</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesblocktemplateutilsphp">trunk/src/wp-includes/block-template-utils.php</a></li>
<li><a href="#trunksrcwpincludesblocksphp">trunk/src/wp-includes/blocks.php</a></li>
<li><a href="#trunksrcwpincludesclasswpblockpatternsregistryphp">trunk/src/wp-includes/class-wp-block-patterns-registry.php</a></li>
<li><a href="#trunktestsphpunittestsblocksserializephp">trunk/tests/phpunit/tests/blocks/serialize.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestblocktypecontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-block-type-controller.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesblocktemplateutilsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/block-template-utils.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/block-template-utils.php    2023-09-21 15:21:45 UTC (rev 56648)
+++ trunk/src/wp-includes/block-template-utils.php      2023-09-21 16:16:05 UTC (rev 56649)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -601,8 +601,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $template->area = $template_file['area'];
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $blocks            = parse_blocks( $template_content );
-       $template->content = traverse_and_serialize_blocks( $blocks, '_inject_theme_attribute_in_template_part_block' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $blocks               = parse_blocks( $template_content );
+       $before_block_visitor = make_before_block_visitor( $template );
+       $after_block_visitor  = make_after_block_visitor( $template );
+       $template->content    = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return $template;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludesblocksphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/blocks.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/blocks.php  2023-09-21 15:21:45 UTC (rev 56648)
+++ trunk/src/wp-includes/blocks.php    2023-09-21 16:16:05 UTC (rev 56649)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -751,6 +751,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered();
</span><span class="cx" style="display: block; padding: 0 10px">        $hooked_blocks = array();
</span><span class="cx" style="display: block; padding: 0 10px">        foreach ( $block_types as $block_type ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! property_exists( $block_type, 'block_hooks' ) || ! is_array( $block_type->block_hooks ) ) {
+                       continue;
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $anchor_block_type === $name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $hooked_blocks[ $block_type->name ] = $relative_position;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -761,6 +764,128 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Returns a function that injects the theme attribute into, and hooked blocks before, a given block.
+ *
+ * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`,
+ * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for
+ * any blocks hooked `before` the given block and as its parent's `first_child`, respectively.
+ *
+ * @since 6.4.0
+ * @access private
+ *
+ * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to.
+ * @return callable A function that returns the serialized markup for the given block,
+ *                  including the markup for any hooked blocks before it.
+ */
+function make_before_block_visitor( $context ) {
+       /**
+        * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup.
+        *
+        * If the current block is a Template Part block, inject the `theme` attribute.
+        * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's
+        * `first_child`, respectively, to the serialized markup for the given block.
+        *
+        * @param array $block  The block to inject the theme attribute into, and hooked blocks before.
+        * @param array $parent The parent block of the given block.
+        * @param array $prev   The previous sibling block of the given block.
+        * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it.
+        */
+       return function( &$block, $parent = null, $prev = null ) use ( $context ) {
+               _inject_theme_attribute_in_template_part_block( $block );
+
+               $markup = '';
+
+               if ( $parent && ! $prev ) {
+                       // Candidate for first-child insertion.
+                       $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] );
+                       foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) {
+                               if ( 'first_child' === $relative_position ) {
+                                       $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
+                                       /** This filter is documented in wp-includes/blocks.php */
+                                       $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context );
+                               }
+                       }
+               }
+
+               $hooked_blocks = get_hooked_blocks( $block['blockName'] );
+               foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) {
+                       if ( 'before' === $relative_position ) {
+                               $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
+                               /**
+                                * Filters the serialized markup of a hooked block.
+                                *
+                                * @since 6.4.0
+                                *
+                                * @param string                  $hooked_block_markup The serialized markup of the hooked block.
+                                * @param string                  $hooked_block_type   The type of the hooked block.
+                                * @param string                  $relative_position   The relative position of the hooked block.
+                                *                                                     Can be one of 'before', 'after', 'first_child', or 'last_child'.
+                                * @param array                   $block               The anchor block.
+                                * @param WP_Block_Template|array $context             The block template, template part, or pattern that the anchor block belongs to.
+                                */
+                               $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context );
+                       }
+               }
+
+               return $markup;
+       };
+}
+
+/**
+ * Returns a function that injects the hooked blocks after a given block.
+ *
+ * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`,
+ * where it will append the markup for any blocks hooked `after` the given block and as its parent's
+ * `last_child`, respectively.
+ *
+ * @since 6.4.0
+ * @access private
+ *
+ * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to.
+ * @return callable A function that returns the serialized markup for the given block,
+ *                  including the markup for any hooked blocks after it.
+ */
+function make_after_block_visitor( $context ) {
+       /**
+        * Injects hooked blocks after the given block, and returns the serialized markup.
+        *
+        * Append the markup for any blocks hooked `after` the given block and as its parent's
+        * `last_child`, respectively, to the serialized markup for the given block.
+        *
+        * @param array $block  The block to inject the hooked blocks after.
+        * @param array $parent The parent block of the given block.
+        * @param array $next   The next sibling block of the given block.
+        * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it.
+        */
+       return function( &$block, $parent = null, $next = null ) use ( $context ) {
+               $markup = '';
+
+               $hooked_blocks = get_hooked_blocks( $block['blockName'] );
+               foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) {
+                       if ( 'after' === $relative_position ) {
+                               $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
+                               /** This filter is documented in wp-includes/blocks.php */
+                               $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context );
+                       }
+               }
+
+               if ( $parent && ! $next ) {
+                       // Candidate for last-child insertion.
+                       $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] );
+                       foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) {
+                               if ( 'last_child' === $relative_position ) {
+                                       $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
+                                       /** This filter is documented in wp-includes/blocks.php */
+                                       $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context );
+                               }
+                       }
+               }
+
+               return $markup;
+       };
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Given an array of attributes, returns a string in the serialized attributes
</span><span class="cx" style="display: block; padding: 0 10px">  * format prepared for post content.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpblockpatternsregistryphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-block-patterns-registry.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-block-patterns-registry.php        2023-09-21 15:21:45 UTC (rev 56648)
+++ trunk/src/wp-includes/class-wp-block-patterns-registry.php  2023-09-21 16:16:05 UTC (rev 56649)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -165,7 +165,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return null;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->registered_patterns[ $pattern_name ];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $pattern              = $this->registered_patterns[ $pattern_name ];
+               $blocks               = parse_blocks( $pattern['content'] );
+               $before_block_visitor = make_before_block_visitor( $pattern );
+               $after_block_visitor  = make_after_block_visitor( $pattern );
+               $pattern['content']   = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
+
+               return $pattern;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -178,11 +184,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *                 and per style.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_all_registered( $outside_init_only = false ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return array_values(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $patterns = array_values(
</ins><span class="cx" style="display: block; padding: 0 10px">                         $outside_init_only
</span><span class="cx" style="display: block; padding: 0 10px">                                ? $this->registered_patterns_outside_init
</span><span class="cx" style="display: block; padding: 0 10px">                                : $this->registered_patterns
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               foreach ( $patterns as $index => $pattern ) {
+                       $blocks                        = parse_blocks( $pattern['content'] );
+                       $before_block_visitor          = make_before_block_visitor( $pattern );
+                       $after_block_visitor           = make_after_block_visitor( $pattern );
+                       $patterns[ $index ]['content'] = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
+               }
+               return $patterns;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsphpunittestsblocksserializephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/blocks/serialize.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/blocks/serialize.php    2023-09-21 15:21:45 UTC (rev 56648)
+++ trunk/tests/phpunit/tests/blocks/serialize.php      2023-09-21 16:16:05 UTC (rev 56649)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -62,7 +62,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers ::traverse_and_serialize_blocks
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_traverse_and_serialize_blocks() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_traverse_and_serialize_blocks_pre_callback_modifies_current_block() {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
</span><span class="cx" style="display: block; padding: 0 10px">                $blocks = parse_blocks( $markup );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -81,6 +81,144 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_inner_block() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_inner_block_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:tests/inserted-block /--><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       /**
+        * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_post_callback_appends_to_inner_block() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_inner_block_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block /-->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       public static function insert_next_to_inner_block_callback( $block ) {
+               if ( 'core/inner' !== $block['blockName'] ) {
+                       return '';
+               }
+
+               return get_comment_delimited_block_content( 'tests/inserted-block', array(), '' );
+       }
+
+       /**
+        * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_child_blocks() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- wp:void /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       /**
+        * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_post_callback_appends_to_child_blocks() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /-->\n\nExample.\n\n<!-- wp:void /--><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       public static function insert_next_to_child_blocks_callback( $block, $parent_block ) {
+               if ( ! isset( $parent_block ) ) {
+                       return '';
+               }
+
+               return get_comment_delimited_block_content(
+                       'tests/inserted-block',
+                       array(
+                               'parent' => $parent_block['blockName'],
+                       ),
+                       ''
+               );
+       }
+
+       /**
+        * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_pre_callback_prepends_if_prev_block() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:tests/inserted-block {\"prev_or_next\":\"core/inner\"} /--><!-- wp:void /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       /**
+        * @ticket 59313
+        *
+        * @covers ::traverse_and_serialize_blocks
+        */
+       public function test_traverse_and_serialize_blocks_post_callback_appends_if_prev_block() {
+               $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
+               $blocks = parse_blocks( $markup );
+
+               $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) );
+
+               $this->assertSame(
+                       "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block {\"prev_or_next\":\"core/void\"} /-->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
+                       $actual
+               );
+       }
+
+       public static function insert_next_to_if_prev_or_next_block_callback( $block, $parent_block, $prev_or_next ) {
+               if ( ! isset( $prev_or_next ) ) {
+                       return '';
+               }
+
+               return get_comment_delimited_block_content(
+                       'tests/inserted-block',
+                       array(
+                               'prev_or_next' => $prev_or_next['blockName'],
+                       ),
+                       ''
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 59327
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 59412
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestblocktypecontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-block-type-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-block-type-controller.php 2023-09-21 15:21:45 UTC (rev 56648)
+++ trunk/tests/phpunit/tests/rest-api/rest-block-type-controller.php   2023-09-21 16:16:05 UTC (rev 56649)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -62,6 +62,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                self::delete_user( self::$admin_id );
</span><span class="cx" style="display: block; padding: 0 10px">                self::delete_user( self::$subscriber_id );
</span><span class="cx" style="display: block; padding: 0 10px">                unregister_block_type( 'fake/test' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                unregister_block_type( 'fake/invalid' );
+               unregister_block_type( 'fake/false' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre>
</div>
</div>

</body>
</html>