PluginProbe
Gutenberg / 23.4.0
Gutenberg v23.4.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 7.4.0 All 402 releases
gutenberg / lib / compat / wordpress-6.9 / block-bindings.php

block-bindings.php in Gutenberg 23.4.0, at lib/compat/wordpress-6.9/block-bindings.php

446 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // Needed for WP_Block_Context_Extractor helper class.
2 /**
3 * Block Bindings: Support for generically setting rich-text block attributes.
4 *
5 * @since 6.9.0
6 * @package gutenberg
7 * @subpackage Block Bindings
8 */
9
10
11 // The following filter can be removed once the minimum required WordPress version is 6.9 or newer.
12 add_filter(
13 'block_bindings_supported_attributes',
14 function ( $attributes, $block_type ) {
15 if ( 'core/image' === $block_type && ! in_array( 'caption', $attributes, true ) ) {
16 $attributes[] = 'caption';
17 }
18 if ( 'core/post-date' === $block_type && ! in_array( 'datetime', $attributes, true ) ) {
19 $attributes[] = 'datetime';
20 }
21 if (
22 in_array( $block_type, array( 'core/navigation-link', 'core/navigation-submenu' ), true ) &&
23 ! in_array( 'url', $attributes, true )
24 ) {
25 $attributes[] = 'url';
26 }
27 return $attributes;
28 },
29 10,
30 2
31 );
32
33 // The following filter can be removed once the minimum required WordPress version is 6.9 or newer.
34 add_filter(
35 'block_editor_settings_all',
36 function ( $editor_settings ) {
37 $editor_settings['__experimentalBlockBindingsSupportedAttributes'] = array();
38 foreach ( array_keys( WP_Block_Type_Registry::get_instance()->get_all_registered() ) as $block_type ) {
39 $supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type );
40 if ( ! empty( $supported_block_attributes ) ) {
41 $editor_settings['__experimentalBlockBindingsSupportedAttributes'][ $block_type ] = $supported_block_attributes;
42 }
43 }
44 return $editor_settings;
45 }
46 );
47
48 /**
49 * Callback function for the render_block filter.
50 *
51 * @since 6.9.0
52 *
53 * @param string $block_content The block content.
54 * @param array $block The full block, including name and attributes.
55 * @param WP_Block $instance The block instance.
56 */
57 function gutenberg_block_bindings_render_block( $block_content, $block, $instance ) {
58 static $inside_block_bindings_render = false;
59 if ( $inside_block_bindings_render ) {
60 return $block_content;
61 }
62
63 // Process the block bindings and get attributes updated with the values from the sources.
64 $computed_attributes = gutenberg_process_block_bindings( $instance );
65 if ( empty( $computed_attributes ) ) {
66 return $block_content;
67 }
68
69 /*
70 * Merge the computed attributes with the original attributes.
71 *
72 * Note that this is not a recursive merge, meaning that nested attributes --
73 * such as block bindings metadata -- will be completely replaced.
74 * This is desirable. At this point, Core has already processed any block
75 * bindings that it supports. What remains to be processed are only the attributes
76 * for which support was added later (through the `block_bindings_supported_attributes`
77 * filter). To do so, we'll run `$instance->render()` once more
78 * so the block can update its content based on those attributes.
79 */
80 $instance->attributes = array_merge( $instance->attributes, $computed_attributes );
81
82 /*
83 * If we're dealing with the Button block, we remove the bindings metadata
84 * in order to avoid having it reprocessed, which would lead to Core
85 * capitalizing the wrapper tag (e.g. <DIV>).
86 */
87 if ( 'core/button' === $instance->name ) {
88 unset( $instance->parsed_block['attrs']['metadata']['bindings'] );
89 }
90
91 /**
92 * This filter (`gutenberg_block_bindings_render_block`) is called from `WP_Block::render()`.
93 * To avoid infinite recursion, we set a flag that this filter checks when invoked which tells
94 * it to exit early.
95 */
96 $inside_block_bindings_render = true;
97 $block_content = $instance->render();
98 $inside_block_bindings_render = false;
99
100 if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {
101 foreach ( $computed_attributes as $attribute_name => $source_value ) {
102 $block_content = gutenberg_replace_html( $block_content, $attribute_name, $source_value, $instance->block_type );
103 }
104 }
105
106 return $block_content;
107 }
108 add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 );
109
110 /**
111 * Retrieves the list of block attributes supported by block bindings.
112 *
113 * @since 6.9.0
114 *
115 * @param string $block_type The block type whose supported attributes are being retrieved.
116 * @return array The list of block attributes that are supported by block bindings.
117 */
118 function gutenberg_get_block_bindings_supported_attributes( $block_type ) {
119 /*
120 * List of block attributes supported by Block Bindings in WP 6.8.
121 * DO NOT MODIFY THIS ARRAY. It's a snapshot of what Core supports in 6.8.
122 * Use the `block_bindings_supported_attributes` filter instead to add support
123 * for new block attributes.
124 */
125 $block_bindings_supported_attributes_6_8 = array(
126 'core/paragraph' => array( 'content' ),
127 'core/heading' => array( 'content' ),
128 'core/image' => array( 'id', 'url', 'title', 'alt' ),
129 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
130 );
131
132 $supported_block_attributes =
133 isset( $block_type, $block_bindings_supported_attributes_6_8[ $block_type ] ) ?
134 $block_bindings_supported_attributes_6_8[ $block_type ] :
135 array();
136
137 /**
138 * Filters the supported block attributes for block bindings.
139 *
140 * @since 6.9.0
141 *
142 * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
143 * @param string $block_type The block type whose attributes are being filtered.
144 */
145 $supported_block_attributes = apply_filters(
146 'block_bindings_supported_attributes',
147 $supported_block_attributes,
148 $block_type
149 );
150
151 /**
152 * Filters the supported block attributes for block bindings.
153 *
154 * The dynamic portion of the hook name, `$block_type`, refers to the block type
155 * whose attributes are being filtered.
156 *
157 * @since 6.9.0
158 *
159 * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
160 */
161 $supported_block_attributes = apply_filters(
162 "block_bindings_supported_attributes_{$block_type}",
163 $supported_block_attributes
164 );
165
166 return $supported_block_attributes;
167 }
168
169 /**
170 * Processes the block bindings and updates the block attributes with the values from the sources.
171 *
172 * A block might contain bindings in its attributes. Bindings are mappings
173 * between an attribute of the block and a source. A "source" is a function
174 * registered with `register_block_bindings_source()` that defines how to
175 * retrieve a value from outside the block, e.g. from post meta.
176 *
177 * This function will process those bindings and update the block's attributes
178 * with the values coming from the bindings.
179 *
180 * ### Example
181 *
182 * The "bindings" property for an Image block might look like this:
183 *
184 * ```json
185 * {
186 * "metadata": {
187 * "bindings": {
188 * "title": {
189 * "source": "core/post-meta",
190 * "args": { "key": "text_custom_field" }
191 * },
192 * "url": {
193 * "source": "core/post-meta",
194 * "args": { "key": "url_custom_field" }
195 * }
196 * }
197 * }
198 * }
199 * ```
200 *
201 * The above example will replace the `title` and `url` attributes of the Image
202 * block with the values of the `text_custom_field` and `url_custom_field` post meta.
203 *
204 * @since 6.9.0
205 *
206 * @param WP_Block $instance The block instance.
207 * @return array The computed block attributes for the provided block bindings.
208 */
209 function gutenberg_process_block_bindings( $instance ) {
210 $block_type = $instance->name;
211 $parsed_block = $instance->parsed_block;
212 $computed_attributes = array();
213
214 /*
215 * List of block attributes supported by Block Bindings in WP 6.8.
216 * DO NOT MODIFY THIS ARRAY. It's a snapshot of what Core supports in 6.8.
217 * Use the `block_bindings_supported_attributes` filter instead to add support
218 * for new block attributes.
219 */
220 $block_bindings_supported_attributes_6_8 = array(
221 'core/paragraph' => array( 'content' ),
222 'core/heading' => array( 'content' ),
223 'core/image' => array( 'id', 'url', 'title', 'alt' ),
224 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
225 );
226
227 $supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type );
228
229 /*
230 * Remove attributes that we know are processed by WP 6.8 from the list,
231 * except if we're dealing with the button block, since WP 6.8 capitalizes its
232 * tag name (e.g. <DIV>).
233 */
234 if ( 'core/button' !== $block_type && isset( $block_type, $block_bindings_supported_attributes_6_8[ $block_type ] ) ) {
235 $supported_block_attributes = array_diff(
236 $supported_block_attributes,
237 $block_bindings_supported_attributes_6_8[ $block_type ]
238 );
239 }
240
241 // If the block doesn't have the bindings property, isn't one of the supported
242 // block types, or the bindings property is not an array, return the block content.
243 if (
244 empty( $supported_block_attributes ) ||
245 empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
246 ! is_array( $parsed_block['attrs']['metadata']['bindings'] )
247 ) {
248 return $computed_attributes;
249 }
250
251 $bindings = $parsed_block['attrs']['metadata']['bindings'];
252
253 /*
254 * If the default binding is set for pattern overrides, replace it
255 * with a pattern override binding for all supported attributes.
256 */
257 if (
258 isset( $bindings['__default']['source'] ) &&
259 'core/pattern-overrides' === $bindings['__default']['source']
260 ) {
261 $updated_bindings = array();
262
263 /*
264 * Build a binding array of all supported attributes.
265 * Note that this also omits the `__default` attribute from the
266 * resulting array.
267 */
268 foreach ( $supported_block_attributes as $attribute_name ) {
269 // Retain any non-pattern override bindings that might be present.
270 $updated_bindings[ $attribute_name ] = $bindings[ $attribute_name ] ?? array( 'source' => 'core/pattern-overrides' );
271 }
272 $bindings = $updated_bindings;
273 /*
274 * Update the bindings metadata of the computed attributes.
275 * This ensures the block receives the expanded __default binding metadata when it renders.
276 */
277 $computed_attributes['metadata'] = array_merge(
278 $parsed_block['attrs']['metadata'],
279 array( 'bindings' => $bindings )
280 );
281 }
282
283 foreach ( $bindings as $attribute_name => $block_binding ) {
284 // If the attribute is not in the supported list, process next attribute.
285 if ( ! in_array( $attribute_name, $supported_block_attributes, true ) ) {
286 continue;
287 }
288 // If no source is provided, or that source is not registered, process next attribute.
289 if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
290 continue;
291 }
292
293 $block_binding_source = get_block_bindings_source( $block_binding['source'] );
294 if ( null === $block_binding_source ) {
295 continue;
296 }
297
298 if ( ! class_exists( 'WP_Block_Context_Extractor' ) ) {
299 // phpcs:ignore Gutenberg.Commenting.SinceTag.MissingClassSinceTag
300 class WP_Block_Context_Extractor extends WP_Block {
301 /**
302 * Static methods of subclasses have access to protected properties
303 * of instances of the parent class.
304 * In this case, this gives us access to `available_context`.
305 */
306 // phpcs:ignore Gutenberg.Commenting.SinceTag.MissingMethodSinceTag
307 public static function get_available_context( $instance ) {
308 return $instance->available_context;
309 }
310 }
311 }
312 $available_context = WP_Block_Context_Extractor::get_available_context( $instance );
313
314 // Adds the necessary context defined by the source.
315 if ( ! empty( $block_binding_source->uses_context ) ) {
316 foreach ( $block_binding_source->uses_context as $context_name ) {
317 if ( array_key_exists( $context_name, $available_context ) ) {
318 $instance->context[ $context_name ] = $available_context[ $context_name ];
319 }
320 }
321 }
322
323 $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
324 $source_value = $block_binding_source->get_value( $source_args, $instance, $attribute_name );
325
326 // If the value is not null, process the HTML based on the block and the attribute.
327 if ( ! is_null( $source_value ) ) {
328 $computed_attributes[ $attribute_name ] = $source_value;
329 }
330 }
331
332 return $computed_attributes;
333 }
334
335 /**
336 * Depending on the block attribute name, replace its value in the HTML based on the value provided.
337 *
338 * @since 6.5.0
339 *
340 * @param string $block_content Block content.
341 * @param string $attribute_name The attribute name to replace.
342 * @param mixed $source_value The value used to replace in the HTML.
343 * @param WP_Block_Type $block_type The block type.
344 * @return string The modified block content.
345 */
346 function gutenberg_replace_html( string $block_content, string $attribute_name, $source_value, WP_Block_Type $block_type ) {
347 if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
348 return $block_content;
349 }
350
351 // Depending on the attribute source, the processing will be different.
352 switch ( $block_type->attributes[ $attribute_name ]['source'] ) {
353 case 'html':
354 case 'rich-text':
355 $block_reader = gutenberg_get_block_bindings_processor( $block_content );
356
357 // TODO: Support for CSS selectors whenever they are ready in the HTML API.
358 // In the meantime, support comma-separated selectors by exploding them into an array.
359 $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] );
360 // Add a bookmark to the first tag to be able to iterate over the selectors.
361 $block_reader->next_tag();
362 $block_reader->set_bookmark( 'iterate-selectors' );
363
364 foreach ( $selectors as $selector ) {
365 // If the parent tag, or any of its children, matches the selector, replace the HTML.
366 if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag(
367 array(
368 'tag_name' => $selector,
369 )
370 ) ) {
371 // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available.
372 $block_reader->release_bookmark( 'iterate-selectors' );
373 $block_reader->replace_rich_text( wp_kses_post( $source_value ) );
374 return $block_reader->get_updated_html();
375 } else {
376 $block_reader->seek( 'iterate-selectors' );
377 }
378 }
379 $block_reader->release_bookmark( 'iterate-selectors' );
380 return $block_content;
381
382 case 'attribute':
383 $amended_content = new WP_HTML_Tag_Processor( $block_content );
384 if ( ! $amended_content->next_tag(
385 array(
386 // TODO: build the query from CSS selector.
387 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'],
388 )
389 ) ) {
390 return $block_content;
391 }
392 $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value );
393 return $amended_content->get_updated_html();
394
395 default:
396 return $block_content;
397 }
398 }
399
400 function gutenberg_get_block_bindings_processor( string $block_content ) {
401 $internal_processor_class = new class('', WP_HTML_Processor::CONSTRUCTOR_UNLOCK_CODE) extends WP_HTML_Processor {
402 /**
403 * Replace the rich text content between a tag opener and matching closer.
404 *
405 * When stopped on a tag opener, replace the content enclosed by it and its
406 * matching closer with the provided rich text.
407 *
408 * @param string $rich_text The rich text to replace the original content with.
409 * @return bool True on success.
410 */
411 // phpcs:ignore Gutenberg.CodeAnalysis.GuardedFunctionAndClassNames.FunctionNotGuardedAgainstRedeclaration
412 public function replace_rich_text( $rich_text ) {
413 if ( $this->is_tag_closer() || ! $this->expects_closer() ) {
414 return false;
415 }
416
417 $depth = $this->get_current_depth();
418
419 $this->set_bookmark( '_wp_block_bindings_tag_opener' );
420 // The bookmark names are prefixed with `_` so the key below has an extra `_`.
421 $tag_opener = $this->bookmarks['__wp_block_bindings_tag_opener'];
422 $start = $tag_opener->start + $tag_opener->length;
423 $this->release_bookmark( '_wp_block_bindings_tag_opener' );
424
425 // Find matching tag closer.
426 while ( $this->next_token() && $this->get_current_depth() >= $depth ) {
427 }
428
429 $this->set_bookmark( '_wp_block_bindings_tag_closer' );
430 $tag_closer = $this->bookmarks['__wp_block_bindings_tag_closer'];
431 $end = $tag_closer->start;
432 $this->release_bookmark( '_wp_block_bindings_tag_closer' );
433
434 $this->lexical_updates[] = new WP_HTML_Text_Replacement(
435 $start,
436 $end - $start,
437 $rich_text
438 );
439
440 return true;
441 }
442 };
443
444 return $internal_processor_class::create_fragment( $block_content );
445 }
446