diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-includes/class-wp-block.php --- a/wp/wp-includes/class-wp-block.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-includes/class-wp-block.php Fri Sep 05 18:40:08 2025 +0200 @@ -12,6 +12,7 @@ * @since 5.5.0 * @property array $attributes */ +#[AllowDynamicProperties] class WP_Block { /** @@ -112,7 +113,16 @@ * * @since 5.5.0 * - * @param array $block Array of parsed block properties. + * @param array $block { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where inner blocks were found. + * } * @param array $available_context Optional array of ancestry context values. * @param WP_Block_Type_Registry $registry Optional block type registry. */ @@ -191,9 +201,227 @@ } /** + * Processes the block bindings and updates the block attributes with the values from the sources. + * + * A block might contain bindings in its attributes. Bindings are mappings + * between an attribute of the block and a source. A "source" is a function + * registered with `register_block_bindings_source()` that defines how to + * retrieve a value from outside the block, e.g. from post meta. + * + * This function will process those bindings and update the block's attributes + * with the values coming from the bindings. + * + * ### Example + * + * The "bindings" property for an Image block might look like this: + * + * ```json + * { + * "metadata": { + * "bindings": { + * "title": { + * "source": "core/post-meta", + * "args": { "key": "text_custom_field" } + * }, + * "url": { + * "source": "core/post-meta", + * "args": { "key": "url_custom_field" } + * } + * } + * } + * } + * ``` + * + * The above example will replace the `title` and `url` attributes of the Image + * block with the values of the `text_custom_field` and `url_custom_field` post meta. + * + * @since 6.5.0 + * @since 6.6.0 Handle the `__default` attribute for pattern overrides. + * + * @return array The computed block attributes for the provided block bindings. + */ + private function process_block_bindings() { + $parsed_block = $this->parsed_block; + $computed_attributes = array(); + $supported_block_attributes = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + // If the block doesn't have the bindings property, isn't one of the supported + // block types, or the bindings property is not an array, return the block content. + if ( + ! isset( $supported_block_attributes[ $this->name ] ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { + return $computed_attributes; + } + + $bindings = $parsed_block['attrs']['metadata']['bindings']; + + /* + * If the default binding is set for pattern overrides, replace it + * with a pattern override binding for all supported attributes. + */ + if ( + isset( $bindings['__default']['source'] ) && + 'core/pattern-overrides' === $bindings['__default']['source'] + ) { + $updated_bindings = array(); + + /* + * Build a binding array of all supported attributes. + * Note that this also omits the `__default` attribute from the + * resulting array. + */ + foreach ( $supported_block_attributes[ $parsed_block['blockName'] ] as $attribute_name ) { + // Retain any non-pattern override bindings that might be present. + $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] ) + ? $bindings[ $attribute_name ] + : array( 'source' => 'core/pattern-overrides' ); + } + $bindings = $updated_bindings; + } + + foreach ( $bindings as $attribute_name => $block_binding ) { + // If the attribute is not in the supported list, process next attribute. + if ( ! in_array( $attribute_name, $supported_block_attributes[ $this->name ], true ) ) { + continue; + } + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { + continue; + } + + $block_binding_source = get_block_bindings_source( $block_binding['source'] ); + if ( null === $block_binding_source ) { + continue; + } + + $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array(); + $source_value = $block_binding_source->get_value( $source_args, $this, $attribute_name ); + + // If the value is not null, process the HTML based on the block and the attribute. + if ( ! is_null( $source_value ) ) { + $computed_attributes[ $attribute_name ] = $source_value; + } + } + + return $computed_attributes; + } + + /** + * Depending on the block attribute name, replace its value in the HTML based on the value provided. + * + * @since 6.5.0 + * + * @param string $block_content Block content. + * @param string $attribute_name The attribute name to replace. + * @param mixed $source_value The value used to replace in the HTML. + * @return string The modified block content. + */ + private function replace_html( string $block_content, string $attribute_name, $source_value ) { + $block_type = $this->block_type; + if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { + return $block_content; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $attribute_name ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $this->name ) { + $button_wrapper = $block_reader->get_tag(); + $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $button_wrapper_attrs = array(); + foreach ( $button_wrapper_attribute_names as $name ) { + $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $block_reader->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $selector_attrs = array(); + foreach ( $selector_attribute_names as $name ) { + $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . ""; + $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); + $amended_content->next_tag(); + foreach ( $selector_attrs as $attribute_key => $attribute_value ) { + $amended_content->set_attribute( $attribute_key, $attribute_value ); + } + if ( 'core/paragraph' === $this->name || 'core/heading' === $this->name ) { + return $amended_content->get_updated_html(); + } + if ( 'core/button' === $this->name ) { + $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; + $amended_button = new WP_HTML_Tag_Processor( $button_markup ); + $amended_button->next_tag(); + foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { + $amended_button->set_attribute( $attribute_key, $attribute_value ); + } + return $amended_button->get_updated_html(); + } + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); + return $amended_content->get_updated_html(); + + default: + return $block_content; + } + } + + + /** * Generates the render output for the block. * * @since 5.5.0 + * @since 6.5.0 Added block bindings processing. + * + * @global WP_Post $post Global post object. * * @param array $options { * Optional options object. @@ -204,6 +432,29 @@ */ public function render( $options = array() ) { global $post; + + /* + * There can be only one root interactive block at a time because the rendered HTML of that block contains + * the rendered HTML of all its inner blocks, including any interactive block. + */ + static $root_interactive_block = null; + /** + * Filters whether Interactivity API should process directives. + * + * @since 6.6.0 + * + * @param bool $enabled Whether the directives processing is enabled. + */ + $interactivity_process_directives_enabled = apply_filters( 'interactivity_process_directives', true ); + if ( + $interactivity_process_directives_enabled && null === $root_interactive_block && ( + ( isset( $this->block_type->supports['interactivity'] ) && true === $this->block_type->supports['interactivity'] ) || + ! empty( $this->block_type->supports['interactivity']['interactive'] ) + ) + ) { + $root_interactive_block = $this; + } + $options = wp_parse_args( $options, array( @@ -211,6 +462,13 @@ ) ); + // Process the block bindings and get attributes updated with the values from the sources. + $computed_attributes = $this->process_block_bindings(); + if ( ! empty( $computed_attributes ) ) { + // Merge the computed attributes with the original attributes. + $this->attributes = array_merge( $this->attributes, $computed_attributes ); + } + $is_dynamic = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic(); $block_content = ''; @@ -241,11 +499,17 @@ $block_content .= $inner_block->render(); } - $index++; + ++$index; } } } + if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) { + foreach ( $computed_attributes as $attribute_name => $source_value ) { + $block_content = $this->replace_html( $block_content, $attribute_name, $source_value ); + } + } + if ( $is_dynamic ) { $global_post = $post; $parent = WP_Block_Supports::$block_to_render; @@ -259,16 +523,34 @@ $post = $global_post; } - if ( ! empty( $this->block_type->script ) ) { - wp_enqueue_script( $this->block_type->script ); + if ( ( ! empty( $this->block_type->script_handles ) ) ) { + foreach ( $this->block_type->script_handles as $script_handle ) { + wp_enqueue_script( $script_handle ); + } + } + + if ( ! empty( $this->block_type->view_script_handles ) ) { + foreach ( $this->block_type->view_script_handles as $view_script_handle ) { + wp_enqueue_script( $view_script_handle ); + } } - if ( ! empty( $this->block_type->view_script ) && empty( $this->block_type->render_callback ) ) { - wp_enqueue_script( $this->block_type->view_script ); + if ( ! empty( $this->block_type->view_script_module_ids ) ) { + foreach ( $this->block_type->view_script_module_ids as $view_script_module_id ) { + wp_enqueue_script_module( $view_script_module_id ); + } } - if ( ! empty( $this->block_type->style ) ) { - wp_enqueue_style( $this->block_type->style ); + if ( ( ! empty( $this->block_type->style_handles ) ) ) { + foreach ( $this->block_type->style_handles as $style_handle ) { + wp_enqueue_style( $style_handle ); + } + } + + if ( ( ! empty( $this->block_type->view_style_handles ) ) ) { + foreach ( $this->block_type->view_style_handles as $view_style_handle ) { + wp_enqueue_style( $view_style_handle ); + } } /** @@ -277,7 +559,7 @@ * @since 5.0.0 * @since 5.9.0 The `$instance` parameter was added. * - * @param string $block_content The block content about to be appended. + * @param string $block_content The block content. * @param array $block The full block, including name and attributes. * @param WP_Block $instance The block instance. */ @@ -292,13 +574,18 @@ * @since 5.7.0 * @since 5.9.0 The `$instance` parameter was added. * - * @param string $block_content The block content about to be appended. + * @param string $block_content The block content. * @param array $block The full block, including name and attributes. * @param WP_Block $instance The block instance. */ $block_content = apply_filters( "render_block_{$this->name}", $block_content, $this->parsed_block, $this ); + if ( $root_interactive_block === $this ) { + // The root interactive block has finished rendering. Time to process directives. + $block_content = wp_interactivity_process_directives( $block_content ); + $root_interactive_block = null; + } + return $block_content; } - }