Skip to content

Commit d455334

Browse files
committed
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 #61074, #62716. git-svn-id: https://develop.svn.wordpress.org/trunk@59838 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 95c00d0 commit d455334

File tree

3 files changed

+255
-35
lines changed

3 files changed

+255
-35
lines changed

src/wp-includes/blocks.php

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,106 @@ function apply_block_hooks_to_content( $content, $context = null, $callback = 'i
11401140
return $content;
11411141
}
11421142

1143+
/**
1144+
* Run the Block Hooks algorithm on a post object's content.
1145+
*
1146+
* This function is different from `apply_block_hooks_to_content` in that
1147+
* it takes ignored hooked block information from the post's metadata into
1148+
* account. This ensures that any blocks hooked as first or last child
1149+
* of the block that corresponds to the post type are handled correctly.
1150+
*
1151+
* @since 6.8.0
1152+
* @access private
1153+
*
1154+
* @param string $content Serialized content.
1155+
* @param WP_Post|null $post A post object that the content belongs to. If set to `null`,
1156+
* `get_post()` will be called to use the current post as context.
1157+
* Default: `null`.
1158+
* @param callable $callback A function that will be called for each block to generate
1159+
* the markup for a given list of blocks that are hooked to it.
1160+
* Default: 'insert_hooked_blocks'.
1161+
* @return string The serialized markup.
1162+
*/
1163+
function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post = null, $callback = 'insert_hooked_blocks' ) {
1164+
// Default to the current post if no context is provided.
1165+
if ( null === $post ) {
1166+
$post = get_post();
1167+
}
1168+
1169+
if ( ! $post instanceof WP_Post ) {
1170+
return apply_block_hooks_to_content( $content, $post, $callback );
1171+
}
1172+
1173+
/*
1174+
* If the content was created using the classic editor or using a single Classic block
1175+
* (`core/freeform`), it might not contain any block markup at all.
1176+
* However, we still might need to inject hooked blocks in the first child or last child
1177+
* positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap
1178+
* the content in a `core/freeform` wrapper block.
1179+
*/
1180+
if ( ! has_blocks( $content ) ) {
1181+
$original_content = $content;
1182+
1183+
$content_wrapped_in_classic_block = get_comment_delimited_block_content(
1184+
'core/freeform',
1185+
array(),
1186+
$content
1187+
);
1188+
1189+
$content = $content_wrapped_in_classic_block;
1190+
}
1191+
1192+
$attributes = array();
1193+
1194+
// If context is a post object, `ignoredHookedBlocks` information is stored in its post meta.
1195+
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
1196+
if ( ! empty( $ignored_hooked_blocks ) ) {
1197+
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
1198+
$attributes['metadata'] = array(
1199+
'ignoredHookedBlocks' => $ignored_hooked_blocks,
1200+
);
1201+
}
1202+
1203+
/*
1204+
* We need to wrap the content in a temporary wrapper block with that metadata
1205+
* so the Block Hooks algorithm can insert blocks that are hooked as first or last child
1206+
* of the wrapper block.
1207+
* To that end, we need to determine the wrapper block type based on the post type.
1208+
*/
1209+
if ( 'wp_navigation' === $post->post_type ) {
1210+
$wrapper_block_type = 'core/navigation';
1211+
} elseif ( 'wp_block' === $post->post_type ) {
1212+
$wrapper_block_type = 'core/block';
1213+
} else {
1214+
$wrapper_block_type = 'core/post-content';
1215+
}
1216+
1217+
$content = get_comment_delimited_block_content(
1218+
$wrapper_block_type,
1219+
$attributes,
1220+
$content
1221+
);
1222+
1223+
// Apply Block Hooks.
1224+
$content = apply_block_hooks_to_content( $content, $post, $callback );
1225+
1226+
// Finally, we need to remove the temporary wrapper block.
1227+
$content = remove_serialized_parent_block( $content );
1228+
1229+
// If we wrapped the content in a `core/freeform` block, we also need to remove that.
1230+
if ( ! empty( $content_wrapped_in_classic_block ) ) {
1231+
/*
1232+
* We cannot simply use remove_serialized_parent_block() here,
1233+
* as that function assumes that the block wrapper is at the top level.
1234+
* However, there might now be a hooked block inserted next to it
1235+
* (as first or last child of the parent).
1236+
*/
1237+
$content = str_replace( $content_wrapped_in_classic_block, $original_content, $content );
1238+
}
1239+
1240+
return $content;
1241+
}
1242+
11431243
/**
11441244
* Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
11451245
*
@@ -1297,57 +1397,32 @@ function insert_hooked_blocks_into_rest_response( $response, $post ) {
12971397
return $response;
12981398
}
12991399

1300-
$attributes = array();
1301-
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
1302-
if ( ! empty( $ignored_hooked_blocks ) ) {
1303-
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
1304-
$attributes['metadata'] = array(
1305-
'ignoredHookedBlocks' => $ignored_hooked_blocks,
1306-
);
1307-
}
1308-
1309-
if ( 'wp_navigation' === $post->post_type ) {
1310-
$wrapper_block_type = 'core/navigation';
1311-
} elseif ( 'wp_block' === $post->post_type ) {
1312-
$wrapper_block_type = 'core/block';
1313-
} else {
1314-
$wrapper_block_type = 'core/post-content';
1315-
}
1316-
1317-
$content = get_comment_delimited_block_content(
1318-
$wrapper_block_type,
1319-
$attributes,
1320-
$response->data['content']['raw']
1321-
);
1322-
1323-
$content = apply_block_hooks_to_content(
1324-
$content,
1400+
$response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object(
1401+
$response->data['content']['raw'],
13251402
$post,
13261403
'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
13271404
);
13281405

1329-
// Remove mock block wrapper.
1330-
$content = remove_serialized_parent_block( $content );
1331-
1332-
$response->data['content']['raw'] = $content;
1333-
13341406
// If the rendered content was previously empty, we leave it like that.
13351407
if ( empty( $response->data['content']['rendered'] ) ) {
13361408
return $response;
13371409
}
13381410

13391411
// `apply_block_hooks_to_content` is called above. Ensure it is not called again as a filter.
1340-
$priority = has_filter( 'the_content', 'apply_block_hooks_to_content' );
1412+
$priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' );
13411413
if ( false !== $priority ) {
1342-
remove_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
1414+
remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority );
13431415
}
13441416

13451417
/** This filter is documented in wp-includes/post-template.php */
1346-
$response->data['content']['rendered'] = apply_filters( 'the_content', $content );
1418+
$response->data['content']['rendered'] = apply_filters(
1419+
'the_content',
1420+
$response->data['content']['raw']
1421+
);
13471422

13481423
// Restore the filter if it was set initially.
13491424
if ( false !== $priority ) {
1350-
add_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
1425+
add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority );
13511426
}
13521427

13531428
return $response;

src/wp-includes/default-filters.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
add_filter( 'the_title', 'convert_chars' );
198198
add_filter( 'the_title', 'trim' );
199199

200-
add_filter( 'the_content', 'apply_block_hooks_to_content', 8 ); // BEFORE do_blocks().
200+
add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 ); // BEFORE do_blocks().
201201
add_filter( 'the_content', 'do_blocks', 9 );
202202
add_filter( 'the_content', 'wptexturize' );
203203
add_filter( 'the_content', 'convert_smilies', 20 );
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
/**
3+
* Tests for the apply_block_hooks_to_content_from_post_object function.
4+
*
5+
* @package WordPress
6+
* @subpackage Blocks
7+
*
8+
* @since 6.8.0
9+
*
10+
* @group blocks
11+
* @group block-hooks
12+
*
13+
* @covers ::apply_block_hooks_to_content_from_post_object
14+
*/
15+
class Tests_Blocks_ApplyBlockHooksToContentFromPostObject extends WP_UnitTestCase {
16+
/**
17+
* Post object.
18+
*
19+
* @var WP_Post
20+
*/
21+
protected static $post;
22+
23+
/**
24+
* Post object.
25+
*
26+
* @var WP_Post
27+
*/
28+
protected static $post_with_ignored_hooked_block;
29+
30+
/**
31+
* Post object.
32+
*
33+
* @var WP_Post
34+
*/
35+
protected static $post_with_non_block_content;
36+
37+
/**
38+
*
39+
* Set up.
40+
*
41+
* @ticket 62716
42+
*/
43+
public static function wpSetUpBeforeClass() {
44+
self::$post = self::factory()->post->create_and_get(
45+
array(
46+
'post_type' => 'post',
47+
'post_status' => 'publish',
48+
'post_title' => 'Test Post',
49+
'post_content' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
50+
)
51+
);
52+
53+
self::$post_with_ignored_hooked_block = self::factory()->post->create_and_get(
54+
array(
55+
'post_type' => 'post',
56+
'post_status' => 'publish',
57+
'post_title' => 'Test Post',
58+
'post_content' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
59+
'meta_input' => array(
60+
'_wp_ignored_hooked_blocks' => '["tests/hooked-block-first-child"]',
61+
),
62+
)
63+
);
64+
65+
self::$post_with_non_block_content = self::factory()->post->create_and_get(
66+
array(
67+
'post_type' => 'post',
68+
'post_status' => 'publish',
69+
'post_title' => 'Test Post',
70+
'post_content' => '<h1>Hello World!</h1>',
71+
)
72+
);
73+
74+
register_block_type(
75+
'tests/hooked-block',
76+
array(
77+
'block_hooks' => array(
78+
'core/heading' => 'after',
79+
),
80+
)
81+
);
82+
83+
register_block_type(
84+
'tests/hooked-block-first-child',
85+
array(
86+
'block_hooks' => array(
87+
'core/post-content' => 'first_child',
88+
),
89+
)
90+
);
91+
}
92+
93+
/**
94+
* Tear down.
95+
*
96+
* @ticket 62716
97+
*/
98+
public static function wpTearDownAfterClass() {
99+
$registry = WP_Block_Type_Registry::get_instance();
100+
101+
$registry->unregister( 'tests/hooked-block' );
102+
$registry->unregister( 'tests/hooked-block-first-child' );
103+
}
104+
105+
/**
106+
* @ticket 62716
107+
*/
108+
public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block() {
109+
$expected = '<!-- wp:tests/hooked-block-first-child /-->' .
110+
self::$post->post_content .
111+
'<!-- wp:tests/hooked-block /-->';
112+
$actual = apply_block_hooks_to_content_from_post_object(
113+
self::$post->post_content,
114+
self::$post,
115+
'insert_hooked_blocks'
116+
);
117+
$this->assertSame( $expected, $actual );
118+
}
119+
120+
/**
121+
* @ticket 62716
122+
*/
123+
public function test_apply_block_hooks_to_content_from_post_object_respects_ignored_hooked_blocks_post_meta() {
124+
$expected = self::$post_with_ignored_hooked_block->post_content . '<!-- wp:tests/hooked-block /-->';
125+
$actual = apply_block_hooks_to_content_from_post_object(
126+
self::$post_with_ignored_hooked_block->post_content,
127+
self::$post_with_ignored_hooked_block,
128+
'insert_hooked_blocks'
129+
);
130+
$this->assertSame( $expected, $actual );
131+
}
132+
133+
/**
134+
* @ticket 62716
135+
*/
136+
public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() {
137+
$expected = '<!-- wp:tests/hooked-block-first-child /-->' . self::$post_with_non_block_content->post_content;
138+
$actual = apply_block_hooks_to_content_from_post_object(
139+
self::$post_with_non_block_content->post_content,
140+
self::$post_with_non_block_content,
141+
'insert_hooked_blocks'
142+
);
143+
$this->assertSame( $expected, $actual );
144+
}
145+
}

0 commit comments

Comments
 (0)