<!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>[59838] trunk: Block Hooks: Add function to encapsulate wrapping in ad-hoc parent.</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/59838">59838</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/59838","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>2025-02-19 14:50:08 +0000 (Wed, 19 Feb 2025)</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'>Block Hooks: Add function to encapsulate wrapping in ad-hoc parent.
Introduce a new function, `apply_block_hooks_to_content_from_post_object`, to colocate the logic used to temporarily wrap content in a parent block (with `ignoredHookedBlocks` information fetched from post meta) alongside the call to `apply_block_hooks_to_content`. Fetching that information from post meta is required for all block types that get their content from post objects, i.e. Post Content, Synced Pattern, and Navigation blocks.
Additionally, the newly introduced function contains logic to ensure that insertion of a hooked block into the `first_child` or `last_child` position of a given Post Content block works, even if that block only contains "classic" markup (i.e. no blocks).
Props bernhard-reiter, gziolo, mamaduka.
Fixes <a href="https://core.trac.wordpress.org/ticket/61074">#61074</a>, <a href="https://core.trac.wordpress.org/ticket/62716">#62716</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesblocksphp">trunk/src/wp-includes/blocks.php</a></li>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsblocksapplyBlockHooksToContentFromPostObjectphp">trunk/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<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 2025-02-18 22:30:05 UTC (rev 59837)
+++ trunk/src/wp-includes/blocks.php 2025-02-19 14:50:08 UTC (rev 59838)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1141,6 +1141,106 @@
</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">+ * Run the Block Hooks algorithm on a post object's content.
+ *
+ * This function is different from `apply_block_hooks_to_content` in that
+ * it takes ignored hooked block information from the post's metadata into
+ * account. This ensures that any blocks hooked as first or last child
+ * of the block that corresponds to the post type are handled correctly.
+ *
+ * @since 6.8.0
+ * @access private
+ *
+ * @param string $content Serialized content.
+ * @param WP_Post|null $post A post object that the content belongs to. If set to `null`,
+ * `get_post()` will be called to use the current post as context.
+ * Default: `null`.
+ * @param callable $callback A function that will be called for each block to generate
+ * the markup for a given list of blocks that are hooked to it.
+ * Default: 'insert_hooked_blocks'.
+ * @return string The serialized markup.
+ */
+function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post = null, $callback = 'insert_hooked_blocks' ) {
+ // Default to the current post if no context is provided.
+ if ( null === $post ) {
+ $post = get_post();
+ }
+
+ if ( ! $post instanceof WP_Post ) {
+ return apply_block_hooks_to_content( $content, $post, $callback );
+ }
+
+ /*
+ * If the content was created using the classic editor or using a single Classic block
+ * (`core/freeform`), it might not contain any block markup at all.
+ * However, we still might need to inject hooked blocks in the first child or last child
+ * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap
+ * the content in a `core/freeform` wrapper block.
+ */
+ if ( ! has_blocks( $content ) ) {
+ $original_content = $content;
+
+ $content_wrapped_in_classic_block = get_comment_delimited_block_content(
+ 'core/freeform',
+ array(),
+ $content
+ );
+
+ $content = $content_wrapped_in_classic_block;
+ }
+
+ $attributes = array();
+
+ // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta.
+ $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
+ if ( ! empty( $ignored_hooked_blocks ) ) {
+ $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
+ $attributes['metadata'] = array(
+ 'ignoredHookedBlocks' => $ignored_hooked_blocks,
+ );
+ }
+
+ /*
+ * We need to wrap the content in a temporary wrapper block with that metadata
+ * so the Block Hooks algorithm can insert blocks that are hooked as first or last child
+ * of the wrapper block.
+ * To that end, we need to determine the wrapper block type based on the post type.
+ */
+ if ( 'wp_navigation' === $post->post_type ) {
+ $wrapper_block_type = 'core/navigation';
+ } elseif ( 'wp_block' === $post->post_type ) {
+ $wrapper_block_type = 'core/block';
+ } else {
+ $wrapper_block_type = 'core/post-content';
+ }
+
+ $content = get_comment_delimited_block_content(
+ $wrapper_block_type,
+ $attributes,
+ $content
+ );
+
+ // Apply Block Hooks.
+ $content = apply_block_hooks_to_content( $content, $post, $callback );
+
+ // Finally, we need to remove the temporary wrapper block.
+ $content = remove_serialized_parent_block( $content );
+
+ // If we wrapped the content in a `core/freeform` block, we also need to remove that.
+ if ( ! empty( $content_wrapped_in_classic_block ) ) {
+ /*
+ * We cannot simply use remove_serialized_parent_block() here,
+ * as that function assumes that the block wrapper is at the top level.
+ * However, there might now be a hooked block inserted next to it
+ * (as first or last child of the parent).
+ */
+ $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content );
+ }
+
+ return $content;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.6.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1297,40 +1397,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return $response;
</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">- $attributes = array();
- $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
- if ( ! empty( $ignored_hooked_blocks ) ) {
- $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
- $attributes['metadata'] = array(
- 'ignoredHookedBlocks' => $ignored_hooked_blocks,
- );
- }
-
- if ( 'wp_navigation' === $post->post_type ) {
- $wrapper_block_type = 'core/navigation';
- } elseif ( 'wp_block' === $post->post_type ) {
- $wrapper_block_type = 'core/block';
- } else {
- $wrapper_block_type = 'core/post-content';
- }
-
- $content = get_comment_delimited_block_content(
- $wrapper_block_type,
- $attributes,
- $response->data['content']['raw']
- );
-
- $content = apply_block_hooks_to_content(
- $content,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object(
+ $response->data['content']['raw'],
</ins><span class="cx" style="display: block; padding: 0 10px"> $post,
</span><span class="cx" style="display: block; padding: 0 10px"> 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
</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">- // Remove mock block wrapper.
- $content = remove_serialized_parent_block( $content );
-
- $response->data['content']['raw'] = $content;
-
</del><span class="cx" style="display: block; padding: 0 10px"> // If the rendered content was previously empty, we leave it like that.
</span><span class="cx" style="display: block; padding: 0 10px"> if ( empty( $response->data['content']['rendered'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> return $response;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1337,17 +1409,20 @@
</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"> // `apply_block_hooks_to_content` is called above. Ensure it is not called again as a filter.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $priority = has_filter( 'the_content', 'apply_block_hooks_to_content' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' );
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( false !== $priority ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- remove_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority );
</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"> /** This filter is documented in wp-includes/post-template.php */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $response->data['content']['rendered'] = apply_filters( 'the_content', $content );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $response->data['content']['rendered'] = apply_filters(
+ 'the_content',
+ $response->data['content']['raw']
+ );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Restore the filter if it was set initially.
</span><span class="cx" style="display: block; padding: 0 10px"> if ( false !== $priority ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- add_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority );
</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"> return $response;
</span></span></pre></div>
<a id="trunksrcwpincludesdefaultfiltersphp"></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/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2025-02-18 22:30:05 UTC (rev 59837)
+++ trunk/src/wp-includes/default-filters.php 2025-02-19 14:50:08 UTC (rev 59838)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -197,7 +197,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_title', 'convert_chars' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_title', 'trim' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'the_content', 'apply_block_hooks_to_content', 8 ); // BEFORE do_blocks().
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 ); // BEFORE do_blocks().
</ins><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'do_blocks', 9 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'wptexturize' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'convert_smilies', 20 );
</span></span></pre></div>
<a id="trunktestsphpunittestsblocksapplyBlockHooksToContentFromPostObjectphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php (rev 0)
+++ trunk/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php 2025-02-19 14:50:08 UTC (rev 59838)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,145 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the apply_block_hooks_to_content_from_post_object function.
+ *
+ * @package WordPress
+ * @subpackage Blocks
+ *
+ * @since 6.8.0
+ *
+ * @group blocks
+ * @group block-hooks
+ *
+ * @covers ::apply_block_hooks_to_content_from_post_object
+ */
+class Tests_Blocks_ApplyBlockHooksToContentFromPostObject extends WP_UnitTestCase {
+ /**
+ * Post object.
+ *
+ * @var WP_Post
+ */
+ protected static $post;
+
+ /**
+ * Post object.
+ *
+ * @var WP_Post
+ */
+ protected static $post_with_ignored_hooked_block;
+
+ /**
+ * Post object.
+ *
+ * @var WP_Post
+ */
+ protected static $post_with_non_block_content;
+
+ /**
+ *
+ * Set up.
+ *
+ * @ticket 62716
+ */
+ public static function wpSetUpBeforeClass() {
+ self::$post = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_title' => 'Test Post',
+ 'post_content' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
+ )
+ );
+
+ self::$post_with_ignored_hooked_block = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_title' => 'Test Post',
+ 'post_content' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
+ 'meta_input' => array(
+ '_wp_ignored_hooked_blocks' => '["tests/hooked-block-first-child"]',
+ ),
+ )
+ );
+
+ self::$post_with_non_block_content = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_title' => 'Test Post',
+ 'post_content' => '<h1>Hello World!</h1>',
+ )
+ );
+
+ register_block_type(
+ 'tests/hooked-block',
+ array(
+ 'block_hooks' => array(
+ 'core/heading' => 'after',
+ ),
+ )
+ );
+
+ register_block_type(
+ 'tests/hooked-block-first-child',
+ array(
+ 'block_hooks' => array(
+ 'core/post-content' => 'first_child',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Tear down.
+ *
+ * @ticket 62716
+ */
+ public static function wpTearDownAfterClass() {
+ $registry = WP_Block_Type_Registry::get_instance();
+
+ $registry->unregister( 'tests/hooked-block' );
+ $registry->unregister( 'tests/hooked-block-first-child' );
+ }
+
+ /**
+ * @ticket 62716
+ */
+ public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block() {
+ $expected = '<!-- wp:tests/hooked-block-first-child /-->' .
+ self::$post->post_content .
+ '<!-- wp:tests/hooked-block /-->';
+ $actual = apply_block_hooks_to_content_from_post_object(
+ self::$post->post_content,
+ self::$post,
+ 'insert_hooked_blocks'
+ );
+ $this->assertSame( $expected, $actual );
+ }
+
+ /**
+ * @ticket 62716
+ */
+ public function test_apply_block_hooks_to_content_from_post_object_respects_ignored_hooked_blocks_post_meta() {
+ $expected = self::$post_with_ignored_hooked_block->post_content . '<!-- wp:tests/hooked-block /-->';
+ $actual = apply_block_hooks_to_content_from_post_object(
+ self::$post_with_ignored_hooked_block->post_content,
+ self::$post_with_ignored_hooked_block,
+ 'insert_hooked_blocks'
+ );
+ $this->assertSame( $expected, $actual );
+ }
+
+ /**
+ * @ticket 62716
+ */
+ public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() {
+ $expected = '<!-- wp:tests/hooked-block-first-child /-->' . self::$post_with_non_block_content->post_content;
+ $actual = apply_block_hooks_to_content_from_post_object(
+ self::$post_with_non_block_content->post_content,
+ self::$post_with_non_block_content,
+ 'insert_hooked_blocks'
+ );
+ $this->assertSame( $expected, $actual );
+ }
+}
</ins></span></pre>
</div>
</div>
</body>
</html>