wp/wp-includes/class-wp-block.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
    10  * Class representing a parsed instance of a block.
    10  * Class representing a parsed instance of a block.
    11  *
    11  *
    12  * @since 5.5.0
    12  * @since 5.5.0
    13  * @property array $attributes
    13  * @property array $attributes
    14  */
    14  */
       
    15 #[AllowDynamicProperties]
    15 class WP_Block {
    16 class WP_Block {
    16 
    17 
    17 	/**
    18 	/**
    18 	 * Original parsed array representation of block.
    19 	 * Original parsed array representation of block.
    19 	 *
    20 	 *
   110 	 * property. Only values which are configured to consumed by the block via
   111 	 * property. Only values which are configured to consumed by the block via
   111 	 * its registered type will be assigned to the block's `context` property.
   112 	 * its registered type will be assigned to the block's `context` property.
   112 	 *
   113 	 *
   113 	 * @since 5.5.0
   114 	 * @since 5.5.0
   114 	 *
   115 	 *
   115 	 * @param array                  $block             Array of parsed block properties.
   116 	 * @param array                  $block             {
       
   117 	 *     A representative array of a single parsed block object. See WP_Block_Parser_Block.
       
   118 	 *
       
   119 	 *     @type string   $blockName    Name of block.
       
   120 	 *     @type array    $attrs        Attributes from block comment delimiters.
       
   121 	 *     @type array    $innerBlocks  List of inner blocks. An array of arrays that
       
   122 	 *                                  have the same structure as this one.
       
   123 	 *     @type string   $innerHTML    HTML from inside block comment delimiters.
       
   124 	 *     @type array    $innerContent List of string fragments and null markers where inner blocks were found.
       
   125 	 * }
   116 	 * @param array                  $available_context Optional array of ancestry context values.
   126 	 * @param array                  $available_context Optional array of ancestry context values.
   117 	 * @param WP_Block_Type_Registry $registry          Optional block type registry.
   127 	 * @param WP_Block_Type_Registry $registry          Optional block type registry.
   118 	 */
   128 	 */
   119 	public function __construct( $block, $available_context = array(), $registry = null ) {
   129 	public function __construct( $block, $available_context = array(), $registry = null ) {
   120 		$this->parsed_block = $block;
   130 		$this->parsed_block = $block;
   189 
   199 
   190 		return null;
   200 		return null;
   191 	}
   201 	}
   192 
   202 
   193 	/**
   203 	/**
       
   204 	 * Processes the block bindings and updates the block attributes with the values from the sources.
       
   205 	 *
       
   206 	 * A block might contain bindings in its attributes. Bindings are mappings
       
   207 	 * between an attribute of the block and a source. A "source" is a function
       
   208 	 * registered with `register_block_bindings_source()` that defines how to
       
   209 	 * retrieve a value from outside the block, e.g. from post meta.
       
   210 	 *
       
   211 	 * This function will process those bindings and update the block's attributes
       
   212 	 * with the values coming from the bindings.
       
   213 	 *
       
   214 	 * ### Example
       
   215 	 *
       
   216 	 * The "bindings" property for an Image block might look like this:
       
   217 	 *
       
   218 	 * ```json
       
   219 	 * {
       
   220 	 *   "metadata": {
       
   221 	 *     "bindings": {
       
   222 	 *       "title": {
       
   223 	 *         "source": "core/post-meta",
       
   224 	 *         "args": { "key": "text_custom_field" }
       
   225 	 *       },
       
   226 	 *       "url": {
       
   227 	 *         "source": "core/post-meta",
       
   228 	 *         "args": { "key": "url_custom_field" }
       
   229 	 *       }
       
   230 	 *     }
       
   231 	 *   }
       
   232 	 * }
       
   233 	 * ```
       
   234 	 *
       
   235 	 * The above example will replace the `title` and `url` attributes of the Image
       
   236 	 * block with the values of the `text_custom_field` and `url_custom_field` post meta.
       
   237 	 *
       
   238 	 * @since 6.5.0
       
   239 	 * @since 6.6.0 Handle the `__default` attribute for pattern overrides.
       
   240 	 *
       
   241 	 * @return array The computed block attributes for the provided block bindings.
       
   242 	 */
       
   243 	private function process_block_bindings() {
       
   244 		$parsed_block               = $this->parsed_block;
       
   245 		$computed_attributes        = array();
       
   246 		$supported_block_attributes = array(
       
   247 			'core/paragraph' => array( 'content' ),
       
   248 			'core/heading'   => array( 'content' ),
       
   249 			'core/image'     => array( 'id', 'url', 'title', 'alt' ),
       
   250 			'core/button'    => array( 'url', 'text', 'linkTarget', 'rel' ),
       
   251 		);
       
   252 
       
   253 		// If the block doesn't have the bindings property, isn't one of the supported
       
   254 		// block types, or the bindings property is not an array, return the block content.
       
   255 		if (
       
   256 			! isset( $supported_block_attributes[ $this->name ] ) ||
       
   257 			empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
       
   258 			! is_array( $parsed_block['attrs']['metadata']['bindings'] )
       
   259 		) {
       
   260 			return $computed_attributes;
       
   261 		}
       
   262 
       
   263 		$bindings = $parsed_block['attrs']['metadata']['bindings'];
       
   264 
       
   265 		/*
       
   266 		 * If the default binding is set for pattern overrides, replace it
       
   267 		 * with a pattern override binding for all supported attributes.
       
   268 		 */
       
   269 		if (
       
   270 			isset( $bindings['__default']['source'] ) &&
       
   271 			'core/pattern-overrides' === $bindings['__default']['source']
       
   272 		) {
       
   273 			$updated_bindings = array();
       
   274 
       
   275 			/*
       
   276 			 * Build a binding array of all supported attributes.
       
   277 			 * Note that this also omits the `__default` attribute from the
       
   278 			 * resulting array.
       
   279 			 */
       
   280 			foreach ( $supported_block_attributes[ $parsed_block['blockName'] ] as $attribute_name ) {
       
   281 				// Retain any non-pattern override bindings that might be present.
       
   282 				$updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] )
       
   283 					? $bindings[ $attribute_name ]
       
   284 					: array( 'source' => 'core/pattern-overrides' );
       
   285 			}
       
   286 			$bindings = $updated_bindings;
       
   287 		}
       
   288 
       
   289 		foreach ( $bindings as $attribute_name => $block_binding ) {
       
   290 			// If the attribute is not in the supported list, process next attribute.
       
   291 			if ( ! in_array( $attribute_name, $supported_block_attributes[ $this->name ], true ) ) {
       
   292 				continue;
       
   293 			}
       
   294 			// If no source is provided, or that source is not registered, process next attribute.
       
   295 			if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
       
   296 				continue;
       
   297 			}
       
   298 
       
   299 			$block_binding_source = get_block_bindings_source( $block_binding['source'] );
       
   300 			if ( null === $block_binding_source ) {
       
   301 				continue;
       
   302 			}
       
   303 
       
   304 			$source_args  = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
       
   305 			$source_value = $block_binding_source->get_value( $source_args, $this, $attribute_name );
       
   306 
       
   307 			// If the value is not null, process the HTML based on the block and the attribute.
       
   308 			if ( ! is_null( $source_value ) ) {
       
   309 				$computed_attributes[ $attribute_name ] = $source_value;
       
   310 			}
       
   311 		}
       
   312 
       
   313 		return $computed_attributes;
       
   314 	}
       
   315 
       
   316 	/**
       
   317 	 * Depending on the block attribute name, replace its value in the HTML based on the value provided.
       
   318 	 *
       
   319 	 * @since 6.5.0
       
   320 	 *
       
   321 	 * @param string $block_content  Block content.
       
   322 	 * @param string $attribute_name The attribute name to replace.
       
   323 	 * @param mixed  $source_value   The value used to replace in the HTML.
       
   324 	 * @return string The modified block content.
       
   325 	 */
       
   326 	private function replace_html( string $block_content, string $attribute_name, $source_value ) {
       
   327 		$block_type = $this->block_type;
       
   328 		if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
       
   329 			return $block_content;
       
   330 		}
       
   331 
       
   332 		// Depending on the attribute source, the processing will be different.
       
   333 		switch ( $block_type->attributes[ $attribute_name ]['source'] ) {
       
   334 			case 'html':
       
   335 			case 'rich-text':
       
   336 				$block_reader = new WP_HTML_Tag_Processor( $block_content );
       
   337 
       
   338 				// TODO: Support for CSS selectors whenever they are ready in the HTML API.
       
   339 				// In the meantime, support comma-separated selectors by exploding them into an array.
       
   340 				$selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] );
       
   341 				// Add a bookmark to the first tag to be able to iterate over the selectors.
       
   342 				$block_reader->next_tag();
       
   343 				$block_reader->set_bookmark( 'iterate-selectors' );
       
   344 
       
   345 				// TODO: This shouldn't be needed when the `set_inner_html` function is ready.
       
   346 				// Store the parent tag and its attributes to be able to restore them later in the button.
       
   347 				// The button block has a wrapper while the paragraph and heading blocks don't.
       
   348 				if ( 'core/button' === $this->name ) {
       
   349 					$button_wrapper                 = $block_reader->get_tag();
       
   350 					$button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
       
   351 					$button_wrapper_attrs           = array();
       
   352 					foreach ( $button_wrapper_attribute_names as $name ) {
       
   353 						$button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name );
       
   354 					}
       
   355 				}
       
   356 
       
   357 				foreach ( $selectors as $selector ) {
       
   358 					// If the parent tag, or any of its children, matches the selector, replace the HTML.
       
   359 					if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag(
       
   360 						array(
       
   361 							'tag_name' => $selector,
       
   362 						)
       
   363 					) ) {
       
   364 						$block_reader->release_bookmark( 'iterate-selectors' );
       
   365 
       
   366 						// TODO: Use `set_inner_html` method whenever it's ready in the HTML API.
       
   367 						// Until then, it is hardcoded for the paragraph, heading, and button blocks.
       
   368 						// Store the tag and its attributes to be able to restore them later.
       
   369 						$selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
       
   370 						$selector_attrs           = array();
       
   371 						foreach ( $selector_attribute_names as $name ) {
       
   372 							$selector_attrs[ $name ] = $block_reader->get_attribute( $name );
       
   373 						}
       
   374 						$selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "</$selector>";
       
   375 						$amended_content = new WP_HTML_Tag_Processor( $selector_markup );
       
   376 						$amended_content->next_tag();
       
   377 						foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
       
   378 							$amended_content->set_attribute( $attribute_key, $attribute_value );
       
   379 						}
       
   380 						if ( 'core/paragraph' === $this->name || 'core/heading' === $this->name ) {
       
   381 							return $amended_content->get_updated_html();
       
   382 						}
       
   383 						if ( 'core/button' === $this->name ) {
       
   384 							$button_markup  = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>";
       
   385 							$amended_button = new WP_HTML_Tag_Processor( $button_markup );
       
   386 							$amended_button->next_tag();
       
   387 							foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
       
   388 								$amended_button->set_attribute( $attribute_key, $attribute_value );
       
   389 							}
       
   390 							return $amended_button->get_updated_html();
       
   391 						}
       
   392 					} else {
       
   393 						$block_reader->seek( 'iterate-selectors' );
       
   394 					}
       
   395 				}
       
   396 				$block_reader->release_bookmark( 'iterate-selectors' );
       
   397 				return $block_content;
       
   398 
       
   399 			case 'attribute':
       
   400 				$amended_content = new WP_HTML_Tag_Processor( $block_content );
       
   401 				if ( ! $amended_content->next_tag(
       
   402 					array(
       
   403 						// TODO: build the query from CSS selector.
       
   404 						'tag_name' => $block_type->attributes[ $attribute_name ]['selector'],
       
   405 					)
       
   406 				) ) {
       
   407 					return $block_content;
       
   408 				}
       
   409 				$amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value );
       
   410 				return $amended_content->get_updated_html();
       
   411 
       
   412 			default:
       
   413 				return $block_content;
       
   414 		}
       
   415 	}
       
   416 
       
   417 
       
   418 	/**
   194 	 * Generates the render output for the block.
   419 	 * Generates the render output for the block.
   195 	 *
   420 	 *
   196 	 * @since 5.5.0
   421 	 * @since 5.5.0
       
   422 	 * @since 6.5.0 Added block bindings processing.
       
   423 	 *
       
   424 	 * @global WP_Post $post Global post object.
   197 	 *
   425 	 *
   198 	 * @param array $options {
   426 	 * @param array $options {
   199 	 *     Optional options object.
   427 	 *     Optional options object.
   200 	 *
   428 	 *
   201 	 *     @type bool $dynamic Defaults to 'true'. Optionally set to false to avoid using the block's render_callback.
   429 	 *     @type bool $dynamic Defaults to 'true'. Optionally set to false to avoid using the block's render_callback.
   202 	 * }
   430 	 * }
   203 	 * @return string Rendered block output.
   431 	 * @return string Rendered block output.
   204 	 */
   432 	 */
   205 	public function render( $options = array() ) {
   433 	public function render( $options = array() ) {
   206 		global $post;
   434 		global $post;
       
   435 
       
   436 		/*
       
   437 		 * There can be only one root interactive block at a time because the rendered HTML of that block contains
       
   438 		 * the rendered HTML of all its inner blocks, including any interactive block.
       
   439 		 */
       
   440 		static $root_interactive_block = null;
       
   441 		/**
       
   442 		 * Filters whether Interactivity API should process directives.
       
   443 		 *
       
   444 		 * @since 6.6.0
       
   445 		 *
       
   446 		 * @param bool $enabled Whether the directives processing is enabled.
       
   447 		 */
       
   448 		$interactivity_process_directives_enabled = apply_filters( 'interactivity_process_directives', true );
       
   449 		if (
       
   450 			$interactivity_process_directives_enabled && null === $root_interactive_block && (
       
   451 				( isset( $this->block_type->supports['interactivity'] ) && true === $this->block_type->supports['interactivity'] ) ||
       
   452 				! empty( $this->block_type->supports['interactivity']['interactive'] )
       
   453 			)
       
   454 		) {
       
   455 			$root_interactive_block = $this;
       
   456 		}
       
   457 
   207 		$options = wp_parse_args(
   458 		$options = wp_parse_args(
   208 			$options,
   459 			$options,
   209 			array(
   460 			array(
   210 				'dynamic' => true,
   461 				'dynamic' => true,
   211 			)
   462 			)
   212 		);
   463 		);
   213 
   464 
       
   465 		// Process the block bindings and get attributes updated with the values from the sources.
       
   466 		$computed_attributes = $this->process_block_bindings();
       
   467 		if ( ! empty( $computed_attributes ) ) {
       
   468 			// Merge the computed attributes with the original attributes.
       
   469 			$this->attributes = array_merge( $this->attributes, $computed_attributes );
       
   470 		}
       
   471 
   214 		$is_dynamic    = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic();
   472 		$is_dynamic    = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic();
   215 		$block_content = '';
   473 		$block_content = '';
   216 
   474 
   217 		if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) {
   475 		if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) {
   218 			$index = 0;
   476 			$index = 0;
   239 						$inner_block->context = apply_filters( 'render_block_context', $inner_block->context, $inner_block->parsed_block, $parent_block );
   497 						$inner_block->context = apply_filters( 'render_block_context', $inner_block->context, $inner_block->parsed_block, $parent_block );
   240 
   498 
   241 						$block_content .= $inner_block->render();
   499 						$block_content .= $inner_block->render();
   242 					}
   500 					}
   243 
   501 
   244 					$index++;
   502 					++$index;
   245 				}
   503 				}
       
   504 			}
       
   505 		}
       
   506 
       
   507 		if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {
       
   508 			foreach ( $computed_attributes as $attribute_name => $source_value ) {
       
   509 				$block_content = $this->replace_html( $block_content, $attribute_name, $source_value );
   246 			}
   510 			}
   247 		}
   511 		}
   248 
   512 
   249 		if ( $is_dynamic ) {
   513 		if ( $is_dynamic ) {
   250 			$global_post = $post;
   514 			$global_post = $post;
   257 			WP_Block_Supports::$block_to_render = $parent;
   521 			WP_Block_Supports::$block_to_render = $parent;
   258 
   522 
   259 			$post = $global_post;
   523 			$post = $global_post;
   260 		}
   524 		}
   261 
   525 
   262 		if ( ! empty( $this->block_type->script ) ) {
   526 		if ( ( ! empty( $this->block_type->script_handles ) ) ) {
   263 			wp_enqueue_script( $this->block_type->script );
   527 			foreach ( $this->block_type->script_handles as $script_handle ) {
   264 		}
   528 				wp_enqueue_script( $script_handle );
   265 
   529 			}
   266 		if ( ! empty( $this->block_type->view_script ) && empty( $this->block_type->render_callback ) ) {
   530 		}
   267 			wp_enqueue_script( $this->block_type->view_script );
   531 
   268 		}
   532 		if ( ! empty( $this->block_type->view_script_handles ) ) {
   269 
   533 			foreach ( $this->block_type->view_script_handles as $view_script_handle ) {
   270 		if ( ! empty( $this->block_type->style ) ) {
   534 				wp_enqueue_script( $view_script_handle );
   271 			wp_enqueue_style( $this->block_type->style );
   535 			}
       
   536 		}
       
   537 
       
   538 		if ( ! empty( $this->block_type->view_script_module_ids ) ) {
       
   539 			foreach ( $this->block_type->view_script_module_ids as $view_script_module_id ) {
       
   540 				wp_enqueue_script_module( $view_script_module_id );
       
   541 			}
       
   542 		}
       
   543 
       
   544 		if ( ( ! empty( $this->block_type->style_handles ) ) ) {
       
   545 			foreach ( $this->block_type->style_handles as $style_handle ) {
       
   546 				wp_enqueue_style( $style_handle );
       
   547 			}
       
   548 		}
       
   549 
       
   550 		if ( ( ! empty( $this->block_type->view_style_handles ) ) ) {
       
   551 			foreach ( $this->block_type->view_style_handles as $view_style_handle ) {
       
   552 				wp_enqueue_style( $view_style_handle );
       
   553 			}
   272 		}
   554 		}
   273 
   555 
   274 		/**
   556 		/**
   275 		 * Filters the content of a single block.
   557 		 * Filters the content of a single block.
   276 		 *
   558 		 *
   277 		 * @since 5.0.0
   559 		 * @since 5.0.0
   278 		 * @since 5.9.0 The `$instance` parameter was added.
   560 		 * @since 5.9.0 The `$instance` parameter was added.
   279 		 *
   561 		 *
   280 		 * @param string   $block_content The block content about to be appended.
   562 		 * @param string   $block_content The block content.
   281 		 * @param array    $block         The full block, including name and attributes.
   563 		 * @param array    $block         The full block, including name and attributes.
   282 		 * @param WP_Block $instance      The block instance.
   564 		 * @param WP_Block $instance      The block instance.
   283 		 */
   565 		 */
   284 		$block_content = apply_filters( 'render_block', $block_content, $this->parsed_block, $this );
   566 		$block_content = apply_filters( 'render_block', $block_content, $this->parsed_block, $this );
   285 
   567 
   290 		 * the block name, e.g. "core/paragraph".
   572 		 * the block name, e.g. "core/paragraph".
   291 		 *
   573 		 *
   292 		 * @since 5.7.0
   574 		 * @since 5.7.0
   293 		 * @since 5.9.0 The `$instance` parameter was added.
   575 		 * @since 5.9.0 The `$instance` parameter was added.
   294 		 *
   576 		 *
   295 		 * @param string   $block_content The block content about to be appended.
   577 		 * @param string   $block_content The block content.
   296 		 * @param array    $block         The full block, including name and attributes.
   578 		 * @param array    $block         The full block, including name and attributes.
   297 		 * @param WP_Block $instance      The block instance.
   579 		 * @param WP_Block $instance      The block instance.
   298 		 */
   580 		 */
   299 		$block_content = apply_filters( "render_block_{$this->name}", $block_content, $this->parsed_block, $this );
   581 		$block_content = apply_filters( "render_block_{$this->name}", $block_content, $this->parsed_block, $this );
   300 
   582 
       
   583 		if ( $root_interactive_block === $this ) {
       
   584 			// The root interactive block has finished rendering. Time to process directives.
       
   585 			$block_content          = wp_interactivity_process_directives( $block_content );
       
   586 			$root_interactive_block = null;
       
   587 		}
       
   588 
   301 		return $block_content;
   589 		return $block_content;
   302 	}
   590 	}
   303 
       
   304 }
   591 }