Make WordPress Core

Changeset 57514


Ignore:
Timestamp:
02/01/2024 12:52:54 PM (3 years ago)
Author:
youknowriad
Message:

Editor: Add the Block Bindings API.

This introduces the Block Bindings API for WordPress.

The API allows developers to connects block attributes to different sources. In this PR, two such sources are included: "post meta" and "pattern". Attributes connected to sources can have their HTML replaced by values coming from the source in a way defined by the binding.

Props czapla, lgladdy, gziolo, sc0ttkclark, swissspidy, artemiosans, kevin940726, fabiankaegy, santosguillamot, talldanwp, wildworks.
Fixes #60282.

Location:
trunk
Files:
5 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-bindings.php

    r57500 r57514  
    2020 * @since 6.5.0
    2121 *
    22  * @param string   $source_name       The name of the source.
     22 * @param string   $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
     23 *                                    `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
     24 *                                    characters, the forward slash `/` and dashes.
    2325 * @param array    $source_properties {
    2426 *     The array of arguments that are used to register a source.
  • trunk/src/wp-includes/class-wp-block-bindings-registry.php

    r57500 r57514  
    4343         * @since 6.5.0
    4444         *
    45          * @param string   $source_name       The name of the source.
     45         * @param string   $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
     46         *                                    `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
     47         *                                    characters, the forward slash `/` and dashes.
    4648         * @param array    $source_properties {
    4749         *     The array of arguments that are used to register a source.
  • trunk/src/wp-includes/class-wp-block.php

    r57493 r57514  
    193193
    194194        /**
     195         * Processes the block bindings in block's attributes.
     196         *
     197         * A block might contain bindings in its attributes. Bindings are mappings
     198         * between an attribute of the block and a source. A "source" is a function
     199         * registered with `register_block_bindings_source()` that defines how to
     200         * retrieve a value from outside the block, e.g. from post meta.
     201         *
     202         * This function will process those bindings and replace the HTML with the value of the binding.
     203         * The value is retrieved from the source of the binding.
     204         *
     205         * ### Example
     206         *
     207         * The "bindings" property for an Image block might look like this:
     208         *
     209         * ```json
     210         * {
     211         *   "metadata": {
     212         *     "bindings": {
     213         *       "title": {
     214         *         "source": "post_meta",
     215         *         "args": { "key": "text_custom_field" }
     216         *       },
     217         *       "url": {
     218         *         "source": "post_meta",
     219         *         "args": { "key": "url_custom_field" }
     220         *       }
     221         *     }
     222         *   }
     223         * }
     224         * ```
     225         *
     226         * The above example will replace the `title` and `url` attributes of the Image
     227         * block with the values of the `text_custom_field` and `url_custom_field` post meta.
     228         *
     229         * @access private
     230         * @since 6.5.0
     231         *
     232         * @param string   $block_content Block content.
     233         * @param array    $block The full block, including name and attributes.
     234         */
     235        private function process_block_bindings( $block_content ) {
     236                $block = $this->parsed_block;
     237
     238                // Allowed blocks that support block bindings.
     239                // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
     240                $allowed_blocks = array(
     241                        'core/paragraph' => array( 'content' ),
     242                        'core/heading'   => array( 'content' ),
     243                        'core/image'     => array( 'url', 'title', 'alt' ),
     244                        'core/button'    => array( 'url', 'text' ),
     245                );
     246
     247                // If the block doesn't have the bindings property, isn't one of the allowed
     248                // block types, or the bindings property is not an array, return the block content.
     249                if ( ! isset( $block['attrs']['metadata']['bindings'] ) ||
     250                                ! is_array( $block['attrs']['metadata']['bindings'] ) ||
     251                                ! isset( $allowed_blocks[ $this->name ] )
     252                                ) {
     253                        return $block_content;
     254                }
     255
     256                $block_bindings_sources = get_all_registered_block_bindings_sources();
     257                $modified_block_content = $block_content;
     258                foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
     259                        // If the attribute is not in the list, process next attribute.
     260                        if ( ! in_array( $binding_attribute, $allowed_blocks[ $this->name ], true ) ) {
     261                                continue;
     262                        }
     263                        // If no source is provided, or that source is not registered, process next attribute.
     264                        if ( ! isset( $binding_source['source'] ) || ! is_string( $binding_source['source'] ) || ! isset( $block_bindings_sources[ $binding_source['source'] ] ) ) {
     265                                continue;
     266                        }
     267
     268                        $source_callback = $block_bindings_sources[ $binding_source['source'] ]['get_value_callback'];
     269                        // Get the value based on the source.
     270                        if ( ! isset( $binding_source['args'] ) ) {
     271                                $source_args = array();
     272                        } else {
     273                                $source_args = $binding_source['args'];
     274                        }
     275                        $source_value = call_user_func_array( $source_callback, array( $source_args, $this, $binding_attribute ) );
     276                        // If the value is null, process next attribute.
     277                        if ( is_null( $source_value ) ) {
     278                                continue;
     279                        }
     280
     281                        // Process the HTML based on the block and the attribute.
     282                        $modified_block_content = $this->replace_html( $modified_block_content, $this->name, $binding_attribute, $source_value );
     283                }
     284                return $modified_block_content;
     285        }
     286
     287        /**
     288         * Depending on the block attributes, replace the HTML based on the value returned by the source.
     289         *
     290         * @since 6.5.0
     291         *
     292         * @param string $block_content Block content.
     293         * @param string $block_name The name of the block to process.
     294         * @param string $block_attr The attribute of the block we want to process.
     295         * @param string $source_value The value used to replace the HTML.
     296         */
     297        private function replace_html( string $block_content, string $block_name, string $block_attr, string $source_value ) {
     298                $block_type = $this->block_type;
     299                if ( null === $block_type || ! isset( $block_type->attributes[ $block_attr ] ) ) {
     300                        return $block_content;
     301                }
     302
     303                // Depending on the attribute source, the processing will be different.
     304                switch ( $block_type->attributes[ $block_attr ]['source'] ) {
     305                        case 'html':
     306                        case 'rich-text':
     307                                $block_reader = new WP_HTML_Tag_Processor( $block_content );
     308
     309                                // TODO: Support for CSS selectors whenever they are ready in the HTML API.
     310                                // In the meantime, support comma-separated selectors by exploding them into an array.
     311                                $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] );
     312                                // Add a bookmark to the first tag to be able to iterate over the selectors.
     313                                $block_reader->next_tag();
     314                                $block_reader->set_bookmark( 'iterate-selectors' );
     315
     316                                // TODO: This shouldn't be needed when the `set_inner_html` function is ready.
     317                                // Store the parent tag and its attributes to be able to restore them later in the button.
     318                                // The button block has a wrapper while the paragraph and heading blocks don't.
     319                                if ( 'core/button' === $block_name ) {
     320                                        $button_wrapper                 = $block_reader->get_tag();
     321                                        $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
     322                                        $button_wrapper_attrs           = array();
     323                                        foreach ( $button_wrapper_attribute_names as $name ) {
     324                                                $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name );
     325                                        }
     326                                }
     327
     328                                foreach ( $selectors as $selector ) {
     329                                        // If the parent tag, or any of its children, matches the selector, replace the HTML.
     330                                        if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag(
     331                                                array(
     332                                                        'tag_name' => $selector,
     333                                                )
     334                                        ) ) {
     335                                                $block_reader->release_bookmark( 'iterate-selectors' );
     336
     337                                                // TODO: Use `set_inner_html` method whenever it's ready in the HTML API.
     338                                                // Until then, it is hardcoded for the paragraph, heading, and button blocks.
     339                                                // Store the tag and its attributes to be able to restore them later.
     340                                                $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
     341                                                $selector_attrs           = array();
     342                                                foreach ( $selector_attribute_names as $name ) {
     343                                                        $selector_attrs[ $name ] = $block_reader->get_attribute( $name );
     344                                                }
     345                                                $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "</$selector>";
     346                                                $amended_content = new WP_HTML_Tag_Processor( $selector_markup );
     347                                                $amended_content->next_tag();
     348                                                foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
     349                                                        $amended_content->set_attribute( $attribute_key, $attribute_value );
     350                                                }
     351                                                if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) {
     352                                                        return $amended_content->get_updated_html();
     353                                                }
     354                                                if ( 'core/button' === $block_name ) {
     355                                                        $button_markup  = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>";
     356                                                        $amended_button = new WP_HTML_Tag_Processor( $button_markup );
     357                                                        $amended_button->next_tag();
     358                                                        foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
     359                                                                $amended_button->set_attribute( $attribute_key, $attribute_value );
     360                                                        }
     361                                                        return $amended_button->get_updated_html();
     362                                                }
     363                                        } else {
     364                                                $block_reader->seek( 'iterate-selectors' );
     365                                        }
     366                                }
     367                                $block_reader->release_bookmark( 'iterate-selectors' );
     368                                return $block_content;
     369
     370                        case 'attribute':
     371                                $amended_content = new WP_HTML_Tag_Processor( $block_content );
     372                                if ( ! $amended_content->next_tag(
     373                                        array(
     374                                                // TODO: build the query from CSS selector.
     375                                                'tag_name' => $block_type->attributes[ $block_attr ]['selector'],
     376                                        )
     377                                ) ) {
     378                                        return $block_content;
     379                                }
     380                                $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) );
     381                                return $amended_content->get_updated_html();
     382                                break;
     383
     384                        default:
     385                                return $block_content;
     386                                break;
     387                }
     388                return;
     389        }
     390
     391
     392        /**
    195393         * Generates the render output for the block.
    196394         *
     
    286484                        }
    287485                }
     486
     487                // Process the block bindings for this block, if any are registered. This
     488                // will replace the block content with the value from a registered binding source.
     489                $block_content = $this->process_block_bindings( $block_content );
    288490
    289491                /**
  • trunk/src/wp-settings.php

    r57503 r57514  
    377377require ABSPATH . WPINC . '/class-wp-script-modules.php';
    378378require ABSPATH . WPINC . '/script-modules.php';
     379require ABSPATH . WPINC . '/block-bindings/sources/post-meta.php';
     380require ABSPATH . WPINC . '/block-bindings/sources/pattern.php';
    379381require ABSPATH . WPINC . '/interactivity-api.php';
    380382
  • trunk/tests/phpunit/includes/functions.php

    r56472 r57514  
    340340 */
    341341function _unhook_block_registration() {
     342        // Block types.
    342343        require __DIR__ . '/unregister-blocks-hooks.php';
    343344        remove_action( 'init', 'register_core_block_types_from_metadata' );
     
    345346        remove_action( 'init', 'register_block_core_widget_group' );
    346347        remove_action( 'init', 'register_core_block_types_from_metadata' );
     348
     349        // Block binding sources.
     350        remove_action( 'init', '_register_block_bindings_pattern_overrides_source' );
     351        remove_action( 'init', '_register_block_bindings_post_meta_source' );
    347352}
    348353tests_add_filter( 'init', '_unhook_block_registration', 1000 );
  • trunk/tests/phpunit/tests/block-bindings/register.php

    r57385 r57514  
    1818
    1919        /**
     20         * Set up before each test.
     21         *
     22         * @since 6.5.0
     23         */
     24        public function set_up() {
     25                foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
     26                        unregister_block_bindings_source( $source_name );
     27                }
     28
     29                parent::set_up();
     30        }
     31
     32        /**
    2033         * Tear down after each test.
    2134         *
     
    2437        public function tear_down() {
    2538                foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
    26                         if ( str_starts_with( $source_name, 'test/' ) ) {
    27                                 unregister_block_bindings_source( $source_name );
    28                         }
     39                        unregister_block_bindings_source( $source_name );
    2940                }
    3041
Note: See TracChangeset for help on using the changeset viewer.