Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Block Hooks (a.k.a. Auto-inserting Blocks) #5158

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/wp-includes/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Comment on lines +527 to +532
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (
'core/template-part' === $block['blockName'] &&
! isset( $block['attrs']['theme'] )
) {
$block['attrs']['theme'] = get_stylesheet();
}
$block = _inject_theme_attribute_in_template_part_block( $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, (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 );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might use separate filters for template and pattern insertion

Suggested change
$block = apply_filters( 'insert_hooked_block', $block, $hooked_blocks[ $hooked_block_type ], $block_template );
$block = apply_filters( 'insert_hooked_block_into_template', $block, $hooked_blocks[ $hooked_block_type ], $block_template );

}
return $block;
};
}

/**
* Parses a block template and removes the theme attribute from each template part.
*
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
119 changes: 115 additions & 4 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) {
'usesContext' => 'uses_context',
'selectors' => 'selectors',
'supports' => 'supports',
'blockHooks' => 'block_hooks',
ockham marked this conversation as resolved.
Show resolved Hide resolved
'styles' => 'styles',
'variations' => 'variations',
'example' => 'example',
Expand Down Expand Up @@ -794,16 +795,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 string $callback Optional. Callback to run on the block before serialization.
ockham marked this conversation as resolved.
Show resolved Hide resolved
* @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'] ) ) {
Expand All @@ -822,12 +829,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 string $callback Optional. Callback to run on the blocks before serialization.
* @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;
}

/**
Expand Down Expand Up @@ -1620,3 +1633,101 @@ 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 ) {
echo "$anchor_block_type $name\n";
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 );
Comment on lines +1691 to +1701
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to implement sibling insertion could be by changing this code by treating the two cases separately (child vs sibling insertion) -- a bit more similar to what we're doing in GB:

	$hooked_blocks = get_hooked_blocks( $block['blockName'] );
	$sibling_hooked_blocks = array_filter( $hooked_blocks, ... ); // position is 'before' or 'after'
	$child_hooked_blocks = array_filter( $hooked_blocks , ... ); // position is 'first_child' or 'last_child'

	foreach ( $child_hooked_blocks as $hooked_block_type => $relative_position ) {
		/* Same implementation as before */
	}

	foreach ( $sibling_hooked_blocks as $hooked_block_type => $relative_position ) {
		$anchor_block_index = array_search( $hooked_block_type, array_column( $block['innerBlocks'], 'blockName' ), true );
		if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) )
 		{
			/* See GB's gutenberg_insert_hooked_block */
		}

(Rough draft; we might want to move some of that logic into insert_hooked_block, if possible.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crucial conceptual difference is that for child insertion, the anchor block is the one that needs to be modified (by inserting the first or last child into its inner blocks), whereas for sibling insertion, it isn't; instead, it is the anchor block's parent -- which is an additional piece of information that we need in that case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are definitely different, the case for first_child and last_child is way simpler to integrate as it can be done on with the new callback param as that's mostly about applying modifications to the $block object in exactly the same way as in the Gutenberg plugin.

I still don't have a clear picture about the sibling insertion so whatever works, should be implemented initially. We can discuss further optimization later, but I can't tell based on the abstract code as it's a very complex use case. I'm still impressed how you managed to integrate it using only existing filters in the plugin.

}
return $block;
}

/**
* Auto-insert a block next to a given "anchor" block.
ockham marked this conversation as resolved.
Show resolved Hide resolved
*
* 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.
ockham marked this conversation as resolved.
Show resolved Hide resolved
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;
}
14 changes: 14 additions & 0 deletions src/wp-includes/class-wp-block-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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|null
gziolo marked this conversation as resolved.
Show resolved Hide resolved
*/
public $block_hooks = array();

/**
* Deprecated block type properties for script and style handles.
*
Expand Down Expand Up @@ -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()
*
Expand All @@ -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|null $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.
Expand Down