diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index b8538ee3b43e2..6f8a6f054b45b 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -513,6 +513,44 @@ function _inject_theme_attribute_in_block_template_content( $template_content ) return $template_content; } +/** + * Returns a function that... + * + * @since 6.4.0 + * @access private + * + * @param WP_Block_Template $block_template a block template. + * @return callable A function that returns a block. + */ +function _parsed_block_visitor( $block_template ) { + return function( $block ) use ( $block_template ) { + if ( + 'core/template-part' === $block['blockName'] && + ! isset( $block['attrs']['theme'] ) + ) { + $block['attrs']['theme'] = get_stylesheet(); + } + + $hooked_blocks = get_hooked_blocks( $block['blockName'] ); + foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { + $hooked_block = array( + 'blockName' => $hooked_block_type, + 'attrs' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), + ); + // Need to pass full current block, (potentially its parent -- for sibiling insertion), relative position, and hooked_block. + $block = insert_hooked_block( $hooked_block, $relative_position, $block ); + /** + * This filter allows modifiying the auto-inserting behavior... + */ + $block = apply_filters( 'insert_hooked_block', $block, $hooked_blocks[ $hooked_block_type ], $block_template ); + } + return $block; + }; +} + /** * Parses a block template and removes the theme attribute from each template part. * @@ -565,7 +603,6 @@ function _build_block_template_result_from_file( $template_file, $template_type $template = new WP_Block_Template(); $template->id = $theme . '//' . $template_file['slug']; $template->theme = $theme; - $template->content = _inject_theme_attribute_in_block_template_content( $template_content ); $template->slug = $template_file['slug']; $template->source = 'theme'; $template->type = $template_type; @@ -589,6 +626,10 @@ function _build_block_template_result_from_file( $template_file, $template_type $template->area = $template_file['area']; } + $blocks = parse_blocks( $template_content ); + $visitor = _parsed_block_visitor( $template ); + $template->content = serialize_blocks( $blocks, $visitor ); + return $template; } diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 4d3a34e34fa77..23d25b0a768a9 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -513,6 +513,39 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { } } + if ( ! empty( $metadata['blockHooks'] ) ) { + /** + * Map camelCased position string (from block.json) to snake_cased block type position. + * + * @var array + */ + $position_mappings = array( + 'before' => 'before', + 'after' => 'after', + 'firstChild' => 'first_child', + 'lastChild' => 'last_child', + ); + + $settings['block_hooks'] = array(); + foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { + // Avoid infinite recursion (hooking to itself). + if ( $metadata['name'] === $anchor_block_name ) { + _doing_it_wrong( + __METHOD__, + __( 'Cannot hook block to itself.', 'gutenberg' ), + '6.4.0' + ); + continue; + } + + if ( ! isset( $position_mappings[ $position ] ) ) { + continue; + } + + $settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; + } + } + if ( ! empty( $metadata['render'] ) ) { $template_path = wp_normalize_path( realpath( @@ -794,16 +827,22 @@ function get_comment_delimited_block_content( $block_name, $block_attributes, $b * instead preserve the markup as parsed. * * @since 5.3.1 + * @since 6.4.0 The `$callback` parameter was added. * - * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. + * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. + * @param string $callback Optional. Callback to run on the block before serialization. Default null. * @return string String of rendered HTML. */ -function serialize_block( $block ) { +function serialize_block( $block, $callback = null ) { + if ( is_callable( $callback ) ) { + $block = call_user_func( $callback, $block ); + } + $block_content = ''; $index = 0; foreach ( $block['innerContent'] as $chunk ) { - $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); + $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ], $callback ); } if ( ! is_array( $block['attrs'] ) ) { @@ -822,12 +861,18 @@ function serialize_block( $block ) { * parsed blocks. * * @since 5.3.1 + * @since 6.4.0 The `$callback` parameter was added. * - * @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). + * @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). + * @param string $callback Optional. Callback to run on the blocks before serialization. Default null. * @return string String of rendered HTML. */ -function serialize_blocks( $blocks ) { - return implode( '', array_map( 'serialize_block', $blocks ) ); +function serialize_blocks( $blocks, $callback = null ) { + $result = ''; + foreach ( $blocks as $block ) { + $result .= serialize_block( $block, $callback ); + }; + return $result; } /** @@ -1620,3 +1665,102 @@ function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { } return null; } + +/** + * Retrieves block types (and positions) hooked into the given block. + * + * @since 6.4.0 + * + * @param string $name Block type name including namespace. + * @return array Associative array of `$block_type_name => $position` pairs. + */ +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 ) { + foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { + if ( $anchor_block_type === $name ) { + $hooked_blocks[ $block_type->name ] = $relative_position; + } + } + } + return $hooked_blocks; +} + +function insert_hooked_blocks( $block ) { + $hooked_blocks = get_hooked_blocks( $block['blockName'] ); + foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { + $hooked_block = array( + 'blockName' => $hooked_block_type, + 'attrs' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), + ); + // Need to pass full current block, (possibly its parent), relative position, and hooked_block. + $block = insert_hooked_block( $hooked_block, $relative_position, $block ); + } + return $block; +} + +/** + * Auto-inserts a block next to a given "anchor" block. + * + * This is a helper function used in the implementation of block hooks. + * It is not meant for public use. + * + * The auto-inserted block can be inserted before or after the anchor block, + * or as the first or last child of the anchor block. + * + * Note that the this mutates the automatically inserted block's + * designated parent block by inserting into the parent's `innerBlocks` array, + * and by updating the parent's `innerContent` array accordingly. + * + * @param array $inserted_block The block to insert. + * @param string $relative_position The position relative to the given block. + * Can be 'before', 'after', 'first_child', or 'last_child'. + * @param array $anchor_block The automatically inserted block will be inserted next to instances of this block type. + * @return array The modified anchor block. + */ +function insert_hooked_block( $inserted_block, $relative_position, $anchor_block ) { + if ( 'first_child' === $relative_position ) { + array_unshift( $anchor_block['innerBlocks'], $inserted_block ); + /* + * Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) + * when rendering blocks, we also need to prepend a value (`null`, to mark a block + * location) to that array. + */ + array_unshift( $anchor_block['innerContent'], null ); + } elseif ( 'last_child' === $relative_position ) { + array_push( $anchor_block['innerBlocks'], $inserted_block ); + // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) + // when rendering blocks, we also need to prepend a value (`null`, to mark a block + // location) to that array. + array_push( $anchor_block['innerContent'], null ); + } + return $anchor_block; + + // TODO: Implement sibling insertion. We might need to pass the anchor's parent block for this to work. + // + // $anchor_block_index = array_search( $anchor_block, $anchor_block_parent['innerBlocks'], true ); // Will this work? + // if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) { + // if ( 'after' === $relative_position ) { + // $anchor_block_index++; + // } + // array_splice( $anchor_block_parent['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) ); + + // // Find matching `innerContent` chunk index. + // $chunk_index = 0; + // while ( $anchor_block_index > 0 ) { + // if ( ! is_string( $anchor_block_parent['innerContent'][ $chunk_index ] ) ) { + // $anchor_block_index--; + // } + // $chunk_index++; + // } + // // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) + // // when rendering blocks, we also need to insert a value (`null`, to mark a block + // // location) into that array. + // array_splice( $anchor_block_parent['innerContent'], $chunk_index, 0, array( null ) ); + // } + // return $anchor_block; +} diff --git a/src/wp-includes/class-wp-block-patterns-registry.php b/src/wp-includes/class-wp-block-patterns-registry.php index a83b35185b671..8e03b795a1506 100644 --- a/src/wp-includes/class-wp-block-patterns-registry.php +++ b/src/wp-includes/class-wp-block-patterns-registry.php @@ -165,7 +165,11 @@ 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'] ); + $visitor = _parsed_block_visitor( $pattern ); // TODO: Should we use different functions for template vs pattern? + $pattern['content'] = serialize_blocks( $blocks, $visitor ); + return $pattern; } /** @@ -178,11 +182,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'] ); + $visitor = _parsed_block_visitor( $pattern ); // TODO: Should we use different functions for template vs pattern? + $patterns[ $index ]['content'] = serialize_blocks( $blocks, $visitor ); + } + + return $patterns; } /** diff --git a/src/wp-includes/class-wp-block-type.php b/src/wp-includes/class-wp-block-type.php index e28bacd3b211c..8aac244cd6f53 100644 --- a/src/wp-includes/class-wp-block-type.php +++ b/src/wp-includes/class-wp-block-type.php @@ -213,6 +213,18 @@ class WP_Block_Type { */ public $style_handles = array(); + /** + * Block hooks for this block type. + * + * A block hook is specified by a block type and a relative position. + * The hooked block will be automatically inserted in the given position + * next to the "anchor" block whenever the latter is encountered. + * + * @since 6.4.0 + * @var array + */ + public $block_hooks = array(); + /** * Deprecated block type properties for script and style handles. * @@ -254,6 +266,7 @@ class WP_Block_Type { * `editor_style_handles`, and `style_handles` properties. * Deprecated the `editor_script`, `script`, `view_script`, `editor_style`, and `style` properties. * @since 6.3.0 Added the `selectors` property. + * @since 6.4.0 Added the `block_hooks` property. * * @see register_block_type() * @@ -279,6 +292,7 @@ class WP_Block_Type { * @type array[] $variations Block variations. * @type array $selectors Custom CSS selectors for theme.json style generation. * @type array|null $supports Supported features. + * @type array $block_hooks Block hooks. * @type array|null $example Structured data for the block preview. * @type callable|null $render_callback Block type render callback. * @type array|null $attributes Block type attributes property schemas.