PluginProbe
Gutenberg / 16.6.0
Gutenberg v16.6.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / experimental / block-hooks.php

block-hooks.php in Gutenberg 16.6.0, at lib/experimental/block-hooks.php

355 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block hooks.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Register hooked blocks for automatic insertion, based on their block.json metadata.
10 *
11 * @param array $settings Array of determined settings for registering a block type.
12 * @param array $metadata Metadata provided for registering a block type.
13 * @return array Updated settings array.
14 */
15 function gutenberg_add_hooked_blocks( $settings, $metadata ) {
16 if ( ! isset( $metadata['__experimentalBlockHooks'] ) ) {
17 return $settings;
18 }
19 $block_hooks = $metadata['__experimentalBlockHooks'];
20
21 /**
22 * Map the camelCased position string from block.json to the snake_cased block type position
23 * used in the hooked block registration function.
24 *
25 * @var array
26 */
27 $property_mappings = array(
28 'before' => 'before',
29 'after' => 'after',
30 'firstChild' => 'first_child',
31 'lastChild' => 'last_child',
32 );
33
34 $inserted_block_name = $metadata['name'];
35 foreach ( $block_hooks as $anchor_block_name => $position ) {
36 // Avoid infinite recursion (hooking to itself).
37 if ( $inserted_block_name === $anchor_block_name ) {
38 _doing_it_wrong(
39 __METHOD__,
40 __( 'Cannot hook block to itself.', 'gutenberg' ),
41 '6.4.0'
42 );
43 continue;
44 }
45
46 if ( ! isset( $property_mappings[ $position ] ) ) {
47 continue;
48 }
49
50 $mapped_position = $property_mappings[ $position ];
51
52 gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name );
53
54 $settings['block_hooks'][ $anchor_block_name ] = $mapped_position;
55 }
56
57 // Copied from `get_block_editor_server_block_settings()`.
58 $fields_to_pick = array(
59 'api_version' => 'apiVersion',
60 'title' => 'title',
61 'description' => 'description',
62 'icon' => 'icon',
63 'attributes' => 'attributes',
64 'provides_context' => 'providesContext',
65 'uses_context' => 'usesContext',
66 'selectors' => 'selectors',
67 'supports' => 'supports',
68 'category' => 'category',
69 'styles' => 'styles',
70 'textdomain' => 'textdomain',
71 'parent' => 'parent',
72 'ancestor' => 'ancestor',
73 'keywords' => 'keywords',
74 'example' => 'example',
75 'variations' => 'variations',
76 );
77 // Add `block_hooks` to the list of fields to pick.
78 $fields_to_pick['block_hooks'] = 'blockHooks';
79
80 $exposed_settings = array_intersect_key( $settings, $fields_to_pick );
81
82 // TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block().
83 wp_add_inline_script(
84 'wp-blocks',
85 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');'
86 );
87
88 return $settings;
89 }
90 add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 );
91
92 /**
93 * Register a hooked block for automatic insertion into a given block hook.
94 *
95 * A block hook is specified by a block type and a relative position. The hooked block
96 * will be automatically inserted in the given position next to the "anchor" block
97 * whenever the latter is encountered. This applies both to the frontend and to the markup
98 * returned by the templates and patterns REST API endpoints.
99 *
100 * This is currently done by filtering parsed blocks as obtained from a block template,
101 * template part, or pattern, and injecting the hooked block where applicable.
102 *
103 * @todo In the long run, we'd likely want some sort of registry for hooked blocks.
104 *
105 * @param string $hooked_block The name of the block to insert.
106 * @param string $position The desired position of the hooked block, relative to its anchor block.
107 * Can be 'before', 'after', 'first_child', or 'last_child'.
108 * @param string $anchor_block The name of the block to insert the hooked block next to.
109 * @return void
110 */
111 function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) {
112 $hooked_block_array = array(
113 'blockName' => $hooked_block,
114 'attrs' => array(),
115 'innerHTML' => '',
116 'innerContent' => array(),
117 'innerBlocks' => array(),
118 );
119
120 $inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block );
121 add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 );
122
123 /*
124 * The block-types REST API controller uses objects of the `WP_Block_Type` class, which are
125 * in turn created upon block type registration. However, that class does not contain
126 * a `block_hooks` property (and is not easily extensible), so we have to use a different
127 * mechanism to communicate to the controller which hooked blocks have been registered for
128 * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to
129 * the controller's response.
130 */
131 $controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block );
132 add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 );
133 }
134
135 /**
136 * Return a function that auto-inserts a block next to a given "anchor" block.
137 *
138 * This is a helper function used in the implementation of block hooks.
139 * It is not meant for public use.
140 *
141 * The auto-inserted block can be inserted before or after the anchor block,
142 * or as the first or last child of the anchor block.
143 *
144 * Note that the returned function mutates the automatically inserted block's
145 * designated parent block by inserting into the parent's `innerBlocks` array,
146 * and by updating the parent's `innerContent` array accordingly.
147 *
148 * @param array $inserted_block The block to insert.
149 * @param string $relative_position The position relative to the given block.
150 * Can be 'before', 'after', 'first_child', or 'last_child'.
151 * @param string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type.
152 * @return callable A function that accepts a block's content and returns the content with the inserted block.
153 */
154 function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) {
155 return function( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) {
156 if ( $anchor_block_type === $block['blockName'] ) {
157 if ( 'first_child' === $relative_position ) {
158 array_unshift( $block['innerBlocks'], $inserted_block );
159 // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
160 // when rendering blocks, we also need to prepend a value (`null`, to mark a block
161 // location) to that array.
162 array_unshift( $block['innerContent'], null );
163 } elseif ( 'last_child' === $relative_position ) {
164 array_push( $block['innerBlocks'], $inserted_block );
165 // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
166 // when rendering blocks, we also need to prepend a value (`null`, to mark a block
167 // location) to that array.
168 array_push( $block['innerContent'], null );
169 }
170 return $block;
171 }
172
173 $anchor_block_index = array_search( $anchor_block_type, array_column( $block['innerBlocks'], 'blockName' ), true );
174 if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) {
175 if ( 'after' === $relative_position ) {
176 $anchor_block_index++;
177 }
178 array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) );
179
180 // Find matching `innerContent` chunk index.
181 $chunk_index = 0;
182 while ( $anchor_block_index > 0 ) {
183 if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) {
184 $anchor_block_index--;
185 }
186 $chunk_index++;
187 }
188 // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
189 // when rendering blocks, we also need to insert a value (`null`, to mark a block
190 // location) into that array.
191 array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
192 }
193 return $block;
194 };
195 }
196
197 /**
198 * Add block hooks information to a block type's controller.
199 *
200 * @param array $inserted_block_type The type of block to insert.
201 * @param string $position The position relative to the anchor block.
202 * Can be 'before', 'after', 'first_child', or 'last_child'.
203 * @param string $anchor_block_type The hooked block will be inserted next to instances of this block type.
204 * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response.
205 */
206 function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) {
207 return function( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) {
208 if ( $block_type->name !== $inserted_block_type ) {
209 return $response;
210 }
211
212 $data = $response->get_data();
213 if ( ! isset( $data['block_hooks'] ) ) {
214 $data['block_hooks'] = array();
215 }
216 $data['block_hooks'][ $anchor_block_type ] = $position;
217 $response->set_data( $data );
218 return $response;
219 };
220 }
221
222 /**
223 * Parse and reserialize block templates to allow running filters.
224 *
225 * By parsing a block template's content and then reserializing it
226 * via `gutenberg_serialize_blocks()`, we are able to run filters
227 * on the parsed blocks. This allows us to modify (parsed) blocks during
228 * depth-first traversal already provided by the serialization process,
229 * rather than having to do so in a separate pass.
230 *
231 * @param WP_Block_Template[] $query_result Array of found block templates.
232 * @return WP_Block_Template[] Updated array of found block templates.
233 */
234 function gutenberg_parse_and_serialize_block_templates( $query_result ) {
235 foreach ( $query_result as $block_template ) {
236 if ( 'custom' === $block_template->source ) {
237 continue;
238 }
239 $blocks = parse_blocks( $block_template->content );
240 $block_template->content = gutenberg_serialize_blocks( $blocks );
241 }
242
243 return $query_result;
244 }
245 add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 );
246
247 /**
248 * Filters the block template object after it has been (potentially) fetched from the theme file.
249 *
250 * By parsing a block template's content and then reserializing it
251 * via `gutenberg_serialize_blocks()`, we are able to run filters
252 * on the parsed blocks. This allows us to modify (parsed) blocks during
253 * depth-first traversal already provided by the serialization process,
254 * rather than having to do so in a separate pass.
255 *
256 * @param WP_Block_Template|null $block_template The found block template, or null if there is none.
257 */
258 function gutenberg_parse_and_serialize_blocks( $block_template ) {
259
260 $blocks = parse_blocks( $block_template->content );
261 $block_template->content = gutenberg_serialize_blocks( $blocks );
262
263 return $block_template;
264 }
265 add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 );
266
267 // Helper functions.
268 // -----------------
269 // The sole purpose of the following two functions (`gutenberg_serialize_block`
270 // and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed
271 // counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter
272 // (also called `gutenberg_serialize_block`) as an entry point for modifications
273 // to the parsed blocks.
274
275 /**
276 * Filterable version of `serialize_block()`.
277 *
278 * This function is identical to `serialize_block()`, except that it applies
279 * the `gutenberg_serialize_block` filter to each block before it is serialized.
280 *
281 * @param array $block The block to be serialized.
282 * @return string The serialized block.
283 *
284 * @see serialize_block()
285 */
286 function gutenberg_serialize_block( $block ) {
287 $block_content = '';
288
289 /**
290 * Filters a parsed block before it is serialized.
291 *
292 * @param array $block The block to be serialized.
293 */
294 $block = apply_filters( 'gutenberg_serialize_block', $block );
295
296 $index = 0;
297 foreach ( $block['innerContent'] as $chunk ) {
298 if ( is_string( $chunk ) ) {
299 $block_content .= $chunk;
300 } else { // Compare to WP_Block::render().
301 $inner_block = $block['innerBlocks'][ $index++ ];
302 $block_content .= gutenberg_serialize_block( $inner_block );
303 }
304 }
305
306 if ( ! is_array( $block['attrs'] ) ) {
307 $block['attrs'] = array();
308 }
309
310 return get_comment_delimited_block_content(
311 $block['blockName'],
312 $block['attrs'],
313 $block_content
314 );
315 }
316
317 /**
318 * Filterable version of `serialize_blocks()`.
319 *
320 * This function is identical to `serialize_blocks()`, except that it applies
321 * the `gutenberg_serialize_block` filter to each block before it is serialized.
322 *
323 * @param array $blocks The blocks to be serialized.
324 * @return string[] The serialized blocks.
325 *
326 * @see serialize_blocks()
327 */
328 function gutenberg_serialize_blocks( $blocks ) {
329 return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) );
330 }
331
332 /**
333 * Register the `block_hooks` field for the block-types REST API controller.
334 *
335 * @return void
336 */
337 function gutenberg_register_block_hooks_rest_field() {
338 register_rest_field(
339 'block-type',
340 'block_hooks',
341 array(
342 'schema' => array(
343 'description' => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ),
344 'patternProperties' => array(
345 '^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
346 'type' => 'string',
347 'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
348 ),
349 ),
350 ),
351 )
352 );
353 }
354 add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' );
355