diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 9e0eafcc99e31..87ad50634c108 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -601,8 +601,10 @@ function _build_block_template_result_from_file( $template_file, $template_type $template->area = $template_file['area']; } - $blocks = parse_blocks( $template_content ); - $template->content = traverse_and_serialize_blocks( $blocks, '_inject_theme_attribute_in_template_part_block' ); + $blocks = parse_blocks( $template_content ); + $before_block_visitor = make_before_block_visitor( $template ); + $after_block_visitor = make_after_block_visitor( $template ); + $template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); return $template; } diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index b7f36cff38c38..4b39ff2400945 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -751,6 +751,9 @@ function get_hooked_blocks( $name ) { $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); $hooked_blocks = array(); foreach ( $block_types as $block_type ) { + if ( ! property_exists( $block_type, 'block_hooks' ) || ! is_array( $block_type->block_hooks ) ) { + continue; + } foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { if ( $anchor_block_type === $name ) { $hooked_blocks[ $block_type->name ] = $relative_position; @@ -760,6 +763,128 @@ function get_hooked_blocks( $name ) { return $hooked_blocks; } +/** + * Returns a function that injects the theme attribute into, and hooked blocks before, a given block. + * + * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`, + * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for + * any blocks hooked `before` the given block and as its parent's `first_child`, respectively. + * + * @since 6.4.0 + * @access private + * + * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to. + * @return callable A function that returns the serialized markup for the given block, + * including the markup for any hooked blocks before it. + */ +function make_before_block_visitor( $context ) { + /** + * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup. + * + * If the current block is a Template Part block, inject the `theme` attribute. + * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's + * `first_child`, respectively, to the serialized markup for the given block. + * + * @param array $block The block to inject the theme attribute into, and hooked blocks before. + * @param array $parent The parent block of the given block. + * @param array $prev The previous sibling block of the given block. + * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it. + */ + return function( &$block, $parent = null, $prev = null ) use ( $context ) { + _inject_theme_attribute_in_template_part_block( $block ); + + $markup = ''; + + if ( $parent && ! $prev ) { + // Candidate for first-child insertion. + $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] ); + foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) { + if ( 'first_child' === $relative_position ) { + $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' ); + /** This filter is documented in wp-includes/blocks.php */ + $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context ); + } + } + } + + $hooked_blocks = get_hooked_blocks( $block['blockName'] ); + foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { + if ( 'before' === $relative_position ) { + $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' ); + /** + * Filters the serialized markup of a hooked block. + * + * @since 6.4.0 + * + * @param string $hooked_block_markup The serialized markup of the hooked block. + * @param string $hooked_block_type The type of the hooked block. + * @param string $relative_position The relative position of the hooked block. + * Can be one of 'before', 'after', 'first_child', or 'last_child'. + * @param array $block The anchor block. + * @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to. + */ + $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context ); + } + } + + return $markup; + }; +} + +/** + * Returns a function that injects the hooked blocks after a given block. + * + * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`, + * where it will append the markup for any blocks hooked `after` the given block and as its parent's + * `last_child`, respectively. + * + * @since 6.4.0 + * @access private + * + * @param WP_Block_Template|array $context A block template, template part, or pattern that the blocks belong to. + * @return callable A function that returns the serialized markup for the given block, + * including the markup for any hooked blocks after it. + */ +function make_after_block_visitor( $context ) { + /** + * Injects hooked blocks after the given block, and returns the serialized markup. + * + * Append the markup for any blocks hooked `after` the given block and as its parent's + * `last_child`, respectively, to the serialized markup for the given block. + * + * @param array $block The block to inject the hooked blocks after. + * @param array $parent The parent block of the given block. + * @param array $next The next sibling block of the given block. + * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it. + */ + return function( &$block, $parent = null, $next = null ) use ( $context ) { + $markup = ''; + + $hooked_blocks = get_hooked_blocks( $block['blockName'] ); + foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { + if ( 'after' === $relative_position ) { + $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' ); + /** This filter is documented in wp-includes/blocks.php */ + $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $block, $context ); + } + } + + if ( $parent && ! $next ) { + // Candidate for last-child insertion. + $hooked_blocks_for_parent = get_hooked_blocks( $parent['blockName'] ); + foreach ( $hooked_blocks_for_parent as $hooked_block_type => $relative_position ) { + if ( 'last_child' === $relative_position ) { + $hooked_block_markup = get_comment_delimited_block_content( $hooked_block_type, array(), '' ); + /** This filter is documented in wp-includes/blocks.php */ + $markup .= apply_filters( 'inject_hooked_block_markup', $hooked_block_markup, $hooked_block_type, $relative_position, $parent, $context ); + } + } + } + + return $markup; + }; +} + /** * Given an array of attributes, returns a string in the serialized attributes * format prepared for post content. diff --git a/src/wp-includes/class-wp-block-patterns-registry.php b/src/wp-includes/class-wp-block-patterns-registry.php index a83b35185b671..56bcd8fd7785c 100644 --- a/src/wp-includes/class-wp-block-patterns-registry.php +++ b/src/wp-includes/class-wp-block-patterns-registry.php @@ -165,7 +165,13 @@ public function get_registered( $pattern_name ) { return null; } - return $this->registered_patterns[ $pattern_name ]; + $pattern = $this->registered_patterns[ $pattern_name ]; + $blocks = parse_blocks( $pattern['content'] ); + $before_block_visitor = make_before_block_visitor( $pattern ); + $after_block_visitor = make_after_block_visitor( $pattern ); + $pattern['content'] = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + + return $pattern; } /** @@ -178,11 +184,19 @@ public function get_registered( $pattern_name ) { * and per style. */ public function get_all_registered( $outside_init_only = false ) { - return array_values( + $patterns = array_values( $outside_init_only ? $this->registered_patterns_outside_init : $this->registered_patterns ); + + foreach ( $patterns as $index => $pattern ) { + $blocks = parse_blocks( $pattern['content'] ); + $before_block_visitor = make_before_block_visitor( $pattern ); + $after_block_visitor = make_after_block_visitor( $pattern ); + $patterns[ $index ]['content'] = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + } + return $patterns; } /** diff --git a/tests/phpunit/tests/blocks/serialize.php b/tests/phpunit/tests/blocks/serialize.php index 484f4c6974675..3b237480c9231 100644 --- a/tests/phpunit/tests/blocks/serialize.php +++ b/tests/phpunit/tests/blocks/serialize.php @@ -62,7 +62,7 @@ public function test_serialized_block_name() { * * @covers ::traverse_and_serialize_blocks */ - public function test_traverse_and_serialize_blocks() { + public function test_traverse_and_serialize_blocks_pre_callback_modifies_current_block() { $markup = "Example.\n\nExample.\n\n"; $blocks = parse_blocks( $markup ); @@ -80,6 +80,144 @@ public static function add_attribute_to_inner_block( &$block ) { } } + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_inner_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_inner_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_to_inner_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_inner_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_inner_block_callback( $block ) { + if ( 'core/inner' !== $block['blockName'] ) { + return ''; + } + + return get_comment_delimited_block_content( 'tests/inserted-block', array(), '' ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_child_blocks() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_to_child_blocks() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_child_blocks_callback( $block, $parent_block ) { + if ( ! isset( $parent_block ) ) { + return ''; + } + + return get_comment_delimited_block_content( + 'tests/inserted-block', + array( + 'parent' => $parent_block['blockName'], + ), + '' + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_if_prev_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_if_prev_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_if_prev_or_next_block_callback( $block, $parent_block, $prev_or_next ) { + if ( ! isset( $prev_or_next ) ) { + return ''; + } + + return get_comment_delimited_block_content( + 'tests/inserted-block', + array( + 'prev_or_next' => $prev_or_next['blockName'], + ), + '' + ); + } + /** * @ticket 59327 * @ticket 59412 diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index b1e1adff14626..2745b61195863 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -62,6 +62,8 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); self::delete_user( self::$subscriber_id ); unregister_block_type( 'fake/test' ); + unregister_block_type( 'fake/invalid' ); + unregister_block_type( 'fake/false' ); } /**