Make WordPress Core

Changeset 59124


Ignore:
Timestamp:
09/30/2024 12:21:31 PM (16 months ago)
Author:
Bernhard Reiter
Message:

Block Hooks: Respect "multiple": false in hooked blocks.

If a prospective hooked block has its multiple block-supports field set to false (thus allowing only one instance of the block to be present), ensure that:

  1. Only one instance of the block will be inserted if it's not yet present in the current context.
  2. The block will not be inserted at all if an instance of it is already present in the current context.

As always in Block Hooks parlance, "context" denotes the containing template, template part, pattern, or navigation post that a hooked block is supposed to be inserted into.

The markup of a webpage that uses a Block Theme typically comprises a number of such contexts -- one template and any number of template parts, patterns, and navigation posts. Note that the limitation imposed by this changeset only applies on a per-context basis, so it's still possible that the resulting page contains more than one instance of a hooked block with "multiple": false set, as each context could contribute up to one such instance.

Props bernhard-reiter, jonsurrell, gziolo.
Fixes #61902.

Location:
trunk
Files:
2 edited

Legend:

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

    r59115 r59124  
    10571057    }
    10581058
    1059     $blocks = parse_blocks( $content );
    1060 
    1061     return traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
     1059    $block_allows_multiple_instances = array();
     1060    /*
     1061     * Remove hooked blocks from `$hooked_block_types` if they have `multiple` set to false and
     1062     * are already present in `$content`.
     1063     */
     1064    foreach ( $hooked_blocks as $anchor_block_type => $relative_positions ) {
     1065        foreach ( $relative_positions as $relative_position => $hooked_block_types ) {
     1066            foreach ( $hooked_block_types as $index => $hooked_block_type ) {
     1067                $hooked_block_type_definition =
     1068                    WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type );
     1069
     1070                $block_allows_multiple_instances[ $hooked_block_type ] =
     1071                    block_has_support( $hooked_block_type_definition, 'multiple', true );
     1072
     1073                if (
     1074                    ! $block_allows_multiple_instances[ $hooked_block_type ] &&
     1075                    has_block( $hooked_block_type, $content )
     1076                ) {
     1077                    unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ][ $index ] );
     1078                }
     1079            }
     1080            if ( empty( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) {
     1081                unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] );
     1082            }
     1083        }
     1084        if ( empty( $hooked_blocks[ $anchor_block_type ] ) ) {
     1085            unset( $hooked_blocks[ $anchor_block_type ] );
     1086        }
     1087    }
     1088
     1089    /*
     1090     * We also need to cover the case where the hooked block is not present in
     1091     * `$content` at first and we're allowed to insert it once -- but not again.
     1092     */
     1093    $suppress_single_instance_blocks = static function ( $hooked_block_types ) use ( &$block_allows_multiple_instances, $content ) {
     1094        static $single_instance_blocks_present_in_content = array();
     1095        foreach ( $hooked_block_types as $index => $hooked_block_type ) {
     1096            if ( ! isset( $block_allows_multiple_instances[ $hooked_block_type ] ) ) {
     1097                $hooked_block_type_definition =
     1098                    WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type );
     1099
     1100                $block_allows_multiple_instances[ $hooked_block_type ] =
     1101                    block_has_support( $hooked_block_type_definition, 'multiple', true );
     1102            }
     1103
     1104            if ( $block_allows_multiple_instances[ $hooked_block_type ] ) {
     1105                continue;
     1106            }
     1107
     1108            // The block doesn't allow multiple instances, so we need to check if it's already present.
     1109            if (
     1110                in_array( $hooked_block_type, $single_instance_blocks_present_in_content, true ) ||
     1111                has_block( $hooked_block_type, $content )
     1112            ) {
     1113                unset( $hooked_block_types[ $index ] );
     1114            } else {
     1115                // We can insert the block once, but need to remember not to insert it again.
     1116                $single_instance_blocks_present_in_content[] = $hooked_block_type;
     1117            }
     1118        }
     1119        return $hooked_block_types;
     1120    };
     1121    add_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX );
     1122    $content = traverse_and_serialize_blocks(
     1123        parse_blocks( $content ),
     1124        $before_block_visitor,
     1125        $after_block_visitor
     1126    );
     1127    remove_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX );
     1128
     1129    return $content;
    10621130}
    10631131
  • trunk/tests/phpunit/tests/blocks/applyBlockHooksToContent.php

    r59101 r59124  
    2828            )
    2929        );
     30
     31        register_block_type(
     32            'tests/hooked-block-with-multiple-false',
     33            array(
     34                'block_hooks' => array(
     35                    'tests/other-anchor-block' => 'after',
     36                ),
     37                'supports'    => array(
     38                    'multiple' => false,
     39                ),
     40            )
     41        );
     42
     43        register_block_type(
     44            'tests/dynamically-hooked-block-with-multiple-false',
     45            array(
     46                'supports' => array(
     47                    'multiple' => false,
     48                ),
     49            )
     50        );
    3051    }
    3152
     
    3960
    4061        $registry->unregister( 'tests/hooked-block' );
     62        $registry->unregister( 'tests/hooked-block-with-multiple-false' );
     63        $registry->unregister( 'tests/dynamically-hooked-block-with-multiple-false' );
    4164    }
    4265
     
    6891        );
    6992    }
     93
     94    /**
     95     * @ticket 61902
     96     */
     97    public function test_apply_block_hooks_to_content_respect_multiple_false() {
     98        $context          = new WP_Block_Template();
     99        $context->content = '<!-- wp:tests/hooked-block-with-multiple-false /--><!-- wp:tests/other-anchor-block /-->';
     100
     101        $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' );
     102        $this->assertSame(
     103            '<!-- wp:tests/hooked-block-with-multiple-false /--><!-- wp:tests/other-anchor-block /-->',
     104            $actual
     105        );
     106    }
     107
     108    /**
     109     * @ticket 61902
     110     */
     111    public function test_apply_block_hooks_to_content_respect_multiple_false_after_inserting_once() {
     112        $context          = new WP_Block_Template();
     113        $context->content = '<!-- wp:tests/other-anchor-block /--><!-- wp:tests/other-block /--><!-- wp:tests/other-anchor-block /-->';
     114
     115        $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' );
     116        $this->assertSame(
     117            '<!-- wp:tests/other-anchor-block /--><!-- wp:tests/hooked-block-with-multiple-false /--><!-- wp:tests/other-block /--><!-- wp:tests/other-anchor-block /-->',
     118            $actual
     119        );
     120    }
     121
     122    /**
     123     * @ticket 61902
     124     */
     125    public function test_apply_block_hooks_to_content_respect_multiple_false_with_filter() {
     126        $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) {
     127            if ( 'tests/yet-another-anchor-block' === $anchor_block_type && 'after' === $relative_position ) {
     128                $hooked_block_types[] = 'tests/dynamically-hooked-block-with-multiple-false';
     129            }
     130
     131            return $hooked_block_types;
     132        };
     133
     134        $context          = new WP_Block_Template();
     135        $context->content = '<!-- wp:tests/dynamically-hooked-block-with-multiple-false /--><!-- wp:tests/yet-another-anchor-block /-->';
     136
     137        add_filter( 'hooked_block_types', $filter, 10, 3 );
     138        $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' );
     139        remove_filter( 'hooked_block_types', $filter, 10 );
     140
     141        $this->assertSame(
     142            '<!-- wp:tests/dynamically-hooked-block-with-multiple-false /--><!-- wp:tests/yet-another-anchor-block /-->',
     143            $actual
     144        );
     145    }
     146
     147    /**
     148     * @ticket 61902
     149     */
     150    public function test_apply_block_hooks_to_content_respect_multiple_false_after_inserting_once_with_filter() {
     151        $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) {
     152            if ( 'tests/yet-another-anchor-block' === $anchor_block_type && 'after' === $relative_position ) {
     153                $hooked_block_types[] = 'tests/dynamically-hooked-block-with-multiple-false';
     154            }
     155
     156            return $hooked_block_types;
     157        };
     158
     159        $context          = new WP_Block_Template();
     160        $context->content = '<!-- wp:tests/yet-another-anchor-block /--><!-- wp:tests/other-block /--><!-- wp:tests/yet-another-anchor-block /-->';
     161
     162        add_filter( 'hooked_block_types', $filter, 10, 3 );
     163        $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' );
     164        remove_filter( 'hooked_block_types', $filter, 10 );
     165
     166        $this->assertSame(
     167            '<!-- wp:tests/yet-another-anchor-block /--><!-- wp:tests/dynamically-hooked-block-with-multiple-false /--><!-- wp:tests/other-block /--><!-- wp:tests/yet-another-anchor-block /-->',
     168            $actual
     169        );
     170    }
    70171}
Note: See TracChangeset for help on using the changeset viewer.