Make WordPress Core

Changeset 56649


Ignore:
Timestamp:
09/21/2023 04:16:05 PM (3 years ago)
Author:
Bernhard Reiter
Message:

Blocks: Implement automatic block insertion into Block Hooks.

Block Hooks allow a third-party block to specify a position relative to a given block into which it will then be automatically inserted (e.g. a "Like" button block can ask to be inserted after the Post Content block, or an eCommerce shopping cart block can ask to be inserted after the Navigation block).

The underlying idea is to provide an extensibility mechanism for Block Themes, in analogy to WordPress' Hooks concept that has allowed extending Classic Themes through filters and actions.

The two core tenets for Block Hooks are:

  1. Insertion into the frontend should happen right after a plugin containing a hooked block is activated (i.e. the user isn't required to insert the block manually in the editor first); similarly, disabling the plugin should remove the hooked block from the frontend.
  2. The user has the ultimate power to customize that automatic insertion: The hooked block is also visible in the editor, and the user's decision to persist, dismiss (i.e. remove), customize, or move it will be respected (and reflected on the frontend).

To account for both tenets, the tradeoff was made to limit automatic block insertion to unmodified templates (and template parts, respectively). The reason for this is that the simplest way of storing the information whether a block has been persisted to (or dismissed from) a given template (or part) is right in the template markup.

To accommodate for that tradeoff, UI controls (toggles) are being added to increase visibility of hooked blocks, and to allow for their later insertion into templates (or parts) that already have been modified by the user.

For hooked blocks to appear both in the frontend and in the editor (see tenet number 2), they need to be inserted into both the frontend markup and the REST API (templates and patterns endpoints) equally. As a consequence, this means that automatic insertion couldn't (only) be implemented at block render stage, as for the editor, the serialized (but unrendered) markup needs to be modified.

Furthermore, hooked blocks also have to be inserted into block patterns. Since practically no filters exist for the patterns registry, this has to be done in the registry's get_registered and get_all_registered methods.

Props gziolo.
Fixes #59313.

Location:
trunk
Files:
5 edited

Legend:

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

    r56644 r56649  
    602602        }
    603603
    604         $blocks            = parse_blocks( $template_content );
    605         $template->content = traverse_and_serialize_blocks( $blocks, '_inject_theme_attribute_in_template_part_block' );
     604        $blocks               = parse_blocks( $template_content );
     605        $before_block_visitor = make_before_block_visitor( $template );
     606        $after_block_visitor  = make_after_block_visitor( $template );
     607        $template->content    = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
    606608
    607609        return $template;
  • trunk/src/wp-includes/blocks.php

    r56644 r56649  
    752752        $hooked_blocks = array();
    753753        foreach ( $block_types as $block_type ) {
     754                if ( ! property_exists( $block_type, 'block_hooks' ) || ! is_array( $block_type->block_hooks ) ) {
     755                        continue;
     756                }
    754757                foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) {
    755758                        if ( $anchor_block_type === $name ) {
     
    759762        }
    760763        return $hooked_blocks;
     764}
     765
     766/**
     767 * Returns a function that injects the theme attribute into, and hooked blocks before, a given block.
     768 *
     769 * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`,
     770 * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for
     771 * any blocks hooked `before` the given block and as its parent's `first_child`, respectively.
     772 *
     773 * @since 6.4.0
     774 * @access private
     775 *
     776 * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to.
     777 * @return callable A function that returns the serialized markup for the given block,
     778 *                  including the markup for any hooked blocks before it.
     779 */
     780function make_before_block_visitor( $context ) {
     781        /**
     782         * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup.
     783         *
     784         * If the current block is a Template Part block, inject the `theme` attribute.
     785         * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's
     786         * `first_child`, respectively, to the serialized markup for the given block.
     787         *
     788         * @param array $block  The block to inject the theme attribute into, and hooked blocks before.
     789         * @param array $parent The parent block of the given block.
     790         * @param array $prev   The previous sibling block of the given block.
     791         * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it.
     792         */
     793        return function( &$block, $parent = null, $prev = null ) use ( $context ) {
     794                _inject_theme_attribute_in_template_part_block( $block );
     795
     796                $markup = '';
     797
     798                if ( $parent && ! $prev ) {
     799                        // Candidate for first-child insertion.
     800                        $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] );
     801                        foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) {
     802                                if ( 'first_child' === $relative_position ) {
     803                                        $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
     804                                        /** This filter is documented in wp-includes/blocks.php */
     805                                        $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context );
     806                                }
     807                        }
     808                }
     809
     810                $hooked_blocks = get_hooked_blocks( $block['blockName'] );
     811                foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) {
     812                        if ( 'before' === $relative_position ) {
     813                                $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
     814                                /**
     815                                 * Filters the serialized markup of a hooked block.
     816                                 *
     817                                 * @since 6.4.0
     818                                 *
     819                                 * @param string                  $hooked_block_markup The serialized markup of the hooked block.
     820                                 * @param string                  $hooked_block_type   The type of the hooked block.
     821                                 * @param string                  $relative_position   The relative position of the hooked block.
     822                                 *                                                     Can be one of 'before', 'after', 'first_child', or 'last_child'.
     823                                 * @param array                   $block               The anchor block.
     824                                 * @param WP_Block_Template|array $context             The block template, template part, or pattern that the anchor block belongs to.
     825                                 */
     826                                $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context );
     827                        }
     828                }
     829
     830                return $markup;
     831        };
     832}
     833
     834/**
     835 * Returns a function that injects the hooked blocks after a given block.
     836 *
     837 * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`,
     838 * where it will append the markup for any blocks hooked `after` the given block and as its parent's
     839 * `last_child`, respectively.
     840 *
     841 * @since 6.4.0
     842 * @access private
     843 *
     844 * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to.
     845 * @return callable A function that returns the serialized markup for the given block,
     846 *                  including the markup for any hooked blocks after it.
     847 */
     848function make_after_block_visitor( $context ) {
     849        /**
     850         * Injects hooked blocks after the given block, and returns the serialized markup.
     851         *
     852         * Append the markup for any blocks hooked `after` the given block and as its parent's
     853         * `last_child`, respectively, to the serialized markup for the given block.
     854         *
     855         * @param array $block  The block to inject the hooked blocks after.
     856         * @param array $parent The parent block of the given block.
     857         * @param array $next   The next sibling block of the given block.
     858         * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it.
     859         */
     860        return function( &$block, $parent = null, $next = null ) use ( $context ) {
     861                $markup = '';
     862
     863                $hooked_blocks = get_hooked_blocks( $block['blockName'] );
     864                foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) {
     865                        if ( 'after' === $relative_position ) {
     866                                $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
     867                                /** This filter is documented in wp-includes/blocks.php */
     868                                $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context );
     869                        }
     870                }
     871
     872                if ( $parent && ! $next ) {
     873                        // Candidate for last-child insertion.
     874                        $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] );
     875                        foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) {
     876                                if ( 'last_child' === $relative_position ) {
     877                                        $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' );
     878                                        /** This filter is documented in wp-includes/blocks.php */
     879                                        $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context );
     880                                }
     881                        }
     882                }
     883
     884                return $markup;
     885        };
    761886}
    762887
  • trunk/src/wp-includes/class-wp-block-patterns-registry.php

    r55693 r56649  
    166166                }
    167167
    168                 return $this->registered_patterns[ $pattern_name ];
     168                $pattern              = $this->registered_patterns[ $pattern_name ];
     169                $blocks               = parse_blocks( $pattern['content'] );
     170                $before_block_visitor = make_before_block_visitor( $pattern );
     171                $after_block_visitor  = make_after_block_visitor( $pattern );
     172                $pattern['content']   = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
     173
     174                return $pattern;
    169175        }
    170176
     
    179185         */
    180186        public function get_all_registered( $outside_init_only = false ) {
    181                 return array_values(
     187                $patterns = array_values(
    182188                        $outside_init_only
    183189                                ? $this->registered_patterns_outside_init
    184190                                : $this->registered_patterns
    185191                );
     192
     193                foreach ( $patterns as $index => $pattern ) {
     194                        $blocks                        = parse_blocks( $pattern['content'] );
     195                        $before_block_visitor          = make_before_block_visitor( $pattern );
     196                        $after_block_visitor           = make_after_block_visitor( $pattern );
     197                        $patterns[ $index ]['content'] = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
     198                }
     199                return $patterns;
    186200        }
    187201
  • trunk/tests/phpunit/tests/blocks/serialize.php

    r56644 r56649  
    6363         * @covers ::traverse_and_serialize_blocks
    6464         */
    65         public function test_traverse_and_serialize_blocks() {
     65        public function test_traverse_and_serialize_blocks_pre_callback_modifies_current_block() {
    6666                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
    6767                $blocks = parse_blocks( $markup );
     
    8282
    8383        /**
     84         * @ticket 59313
     85         *
     86         * @covers ::traverse_and_serialize_blocks
     87         */
     88        public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_inner_block() {
     89                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     90                $blocks = parse_blocks( $markup );
     91
     92                $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_inner_block_callback' ) );
     93
     94                $this->assertSame(
     95                        "<!-- wp:outer --><!-- wp:tests/inserted-block /--><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
     96                        $actual
     97                );
     98        }
     99
     100        /**
     101         * @ticket 59313
     102         *
     103         * @covers ::traverse_and_serialize_blocks
     104         */
     105        public function test_traverse_and_serialize_blocks_post_callback_appends_to_inner_block() {
     106                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     107                $blocks = parse_blocks( $markup );
     108
     109                $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_inner_block_callback' ) );
     110
     111                $this->assertSame(
     112                        "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block /-->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
     113                        $actual
     114                );
     115        }
     116
     117        public static function insert_next_to_inner_block_callback( $block ) {
     118                if ( 'core/inner' !== $block['blockName'] ) {
     119                        return '';
     120                }
     121
     122                return get_comment_delimited_block_content( 'tests/inserted-block', array(), '' );
     123        }
     124
     125        /**
     126         * @ticket 59313
     127         *
     128         * @covers ::traverse_and_serialize_blocks
     129         */
     130        public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_child_blocks() {
     131                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     132                $blocks = parse_blocks( $markup );
     133
     134                $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) );
     135
     136                $this->assertSame(
     137                        "<!-- wp:outer --><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- wp:void /--><!-- /wp:outer -->",
     138                        $actual
     139                );
     140        }
     141
     142        /**
     143         * @ticket 59313
     144         *
     145         * @covers ::traverse_and_serialize_blocks
     146         */
     147        public function test_traverse_and_serialize_blocks_post_callback_appends_to_child_blocks() {
     148                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     149                $blocks = parse_blocks( $markup );
     150
     151                $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) );
     152
     153                $this->assertSame(
     154                        "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /-->\n\nExample.\n\n<!-- wp:void /--><!-- wp:tests/inserted-block {\"parent\":\"core/outer\"} /--><!-- /wp:outer -->",
     155                        $actual
     156                );
     157        }
     158
     159        public static function insert_next_to_child_blocks_callback( $block, $parent_block ) {
     160                if ( ! isset( $parent_block ) ) {
     161                        return '';
     162                }
     163
     164                return get_comment_delimited_block_content(
     165                        'tests/inserted-block',
     166                        array(
     167                                'parent' => $parent_block['blockName'],
     168                        ),
     169                        ''
     170                );
     171        }
     172
     173        /**
     174         * @ticket 59313
     175         *
     176         * @covers ::traverse_and_serialize_blocks
     177         */
     178        public function test_traverse_and_serialize_blocks_pre_callback_prepends_if_prev_block() {
     179                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     180                $blocks = parse_blocks( $markup );
     181
     182                $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) );
     183
     184                $this->assertSame(
     185                        "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:tests/inserted-block {\"prev_or_next\":\"core/inner\"} /--><!-- wp:void /--><!-- /wp:outer -->",
     186                        $actual
     187                );
     188        }
     189
     190        /**
     191         * @ticket 59313
     192         *
     193         * @covers ::traverse_and_serialize_blocks
     194         */
     195        public function test_traverse_and_serialize_blocks_post_callback_appends_if_prev_block() {
     196                $markup = "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner -->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->";
     197                $blocks = parse_blocks( $markup );
     198
     199                $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) );
     200
     201                $this->assertSame(
     202                        "<!-- wp:outer --><!-- wp:inner {\"key\":\"value\"} -->Example.<!-- /wp:inner --><!-- wp:tests/inserted-block {\"prev_or_next\":\"core/void\"} /-->\n\nExample.\n\n<!-- wp:void /--><!-- /wp:outer -->",
     203                        $actual
     204                );
     205        }
     206
     207        public static function insert_next_to_if_prev_or_next_block_callback( $block, $parent_block, $prev_or_next ) {
     208                if ( ! isset( $prev_or_next ) ) {
     209                        return '';
     210                }
     211
     212                return get_comment_delimited_block_content(
     213                        'tests/inserted-block',
     214                        array(
     215                                'prev_or_next' => $prev_or_next['blockName'],
     216                        ),
     217                        ''
     218                );
     219        }
     220
     221        /**
    84222         * @ticket 59327
    85223         * @ticket 59412
  • trunk/tests/phpunit/tests/rest-api/rest-block-type-controller.php

    r56588 r56649  
    6363                self::delete_user( self::$subscriber_id );
    6464                unregister_block_type( 'fake/test' );
     65                unregister_block_type( 'fake/invalid' );
     66                unregister_block_type( 'fake/false' );
    6567        }
    6668
Note: See TracChangeset for help on using the changeset viewer.