Skip to content

Commit

Permalink
Experiment: Auto-inserting blocks on the frontend and in the editor (…
Browse files Browse the repository at this point in the history
…via REST API) (#51449)
  • Loading branch information
ockham committed Jul 26, 2023
1 parent b0e3f20 commit b0fb1c2
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 9 deletions.
19 changes: 11 additions & 8 deletions lib/compat/wordpress-6.3/rest-api.php
Expand Up @@ -85,15 +85,18 @@ function add_modified_wp_template_schema() {
}
add_filter( 'rest_api_init', 'add_modified_wp_template_schema' );

/**
* Registers the block patterns REST API routes.
*/
function gutenberg_register_rest_block_patterns() {
$block_patterns = new Gutenberg_REST_Block_Patterns_Controller_6_3();
$block_patterns->register_routes();
// If the Auto-inserting Blocks experiment is enabled, we load the block patterns
// controller in lib/experimental/rest-api.php instead.
if ( ! gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
/**
* Registers the block patterns REST API routes.
*/
function gutenberg_register_rest_block_patterns() {
$block_patterns = new Gutenberg_REST_Block_Patterns_Controller_6_3();
$block_patterns->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' );
}
add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' );


/**
* Registers the Navigation Fallbacks REST API routes.
Expand Down
246 changes: 246 additions & 0 deletions lib/experimental/auto-inserting-blocks.php
@@ -0,0 +1,246 @@
<?php
/**
* Auto-inserting blocks.
*
* @package gutenberg
*/

/**
* Return a function that auto-inserts blocks relative to a given block.
*
* @param array $inserted_block The block to insert.
* @param string $relative_position The position relative to the given block.
* @param string $anchor_block The block to insert relative to.
* @return callable A function that accepts a block's content and returns the content with the inserted block.
*/
function gutenberg_auto_insert_block( $inserted_block, $relative_position, $anchor_block ) {
return function( $block ) use ( $inserted_block, $relative_position, $anchor_block ) {
if ( $anchor_block === $block['blockName'] ) {
if ( 'first_child' === $relative_position ) {
array_unshift( $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( $block['innerContent'], null );
} elseif ( 'last_child' === $relative_position ) {
array_push( $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( $block['innerContent'], null );
}
return $block;
}

$anchor_block_index = array_search( $anchor_block, array_column( $block['innerBlocks'], 'blockName' ), true );
if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) {
if ( 'after' === $relative_position ) {
$anchor_block_index++;
}
array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) );

// Find matching `innerContent` chunk index.
$chunk_index = 0;
while ( $anchor_block_index > 0 ) {
if ( ! is_string( $block['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( $block['innerContent'], $chunk_index, 0, array( null ) );
}
return $block;
};
}

/**
* Register blocks for auto-insertion, based on their block.json metadata.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
* @return array Updated settings array.
*/
function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
if ( ! isset( $metadata['__experimentalAutoInsert'] ) ) {
return $settings;
}
$auto_insert = $metadata['__experimentalAutoInsert'];

/**
* Map the camelCased position string from block.json to the snake_cased block type position
* used in the auto-inserting block registration function.
*
* @var array
*/
$property_mappings = array(
'before' => 'before',
'after' => 'after',
'firstChild' => 'first_child',
'lastChild' => 'last_child',
);

$inserted_block_name = $metadata['name'];
foreach ( $auto_insert as $anchor_block_name => $position ) {
// Avoid infinite recursion (auto-inserting next to or into self).
if ( $inserted_block_name === $anchor_block_name ) {
_doing_it_wrong(
__METHOD__,
__( 'Cannot auto-insert block next to itself.', 'gutenberg' ),
'6.4.0'
);
continue;
}

if ( ! isset( $property_mappings[ $position ] ) ) {
continue;
}

$mapped_position = $property_mappings[ $position ];

gutenberg_register_auto_inserted_block( $inserted_block_name, $mapped_position, $anchor_block_name );

$settings['auto_insert'][ $anchor_block_name ] = $mapped_position;
}

return $settings;
}
add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 );

/**
* Register block for auto-insertion into the frontend and REST API.
*
* Register a block for auto-insertion into the frontend and into the markup
* returned by the templates and patterns REST API endpoints.
*
* This is currently done by filtering parsed blocks as obtained from a block template
* template part, or pattern and injecting the auto-inserted block where applicable.
*
* @todo In the long run, we'd likely want some sort of registry for auto-inserted blocks.
*
* @param string $inserted_block The name of the block to insert.
* @param string $position The desired position of the auto-inserted block, relative to its anchor block.
* Can be 'before', 'after', 'first_child', or 'last_child'.
* @param string $anchor_block The name of the block to insert the auto-inserted block next to.
* @return void
*/
function gutenberg_register_auto_inserted_block( $inserted_block, $position, $anchor_block ) {
$inserted_block = array(
'blockName' => $inserted_block,
'attrs' => array(),
'innerHTML' => '',
'innerContent' => array(),
'innerBlocks' => array(),
);

$inserter = gutenberg_auto_insert_block( $inserted_block, $position, $anchor_block );
add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 );
}

/**
* Parse and reserialize block templates to allow running filters.
*
* By parsing a block template's content and then reserializing it
* via `gutenberg_serialize_blocks()`, we are able to run filters
* on the parsed blocks.
*
* @param WP_Block_Template[] $query_result Array of found block templates.
* @return WP_Block_Template[] Updated array of found block templates.
*/
function gutenberg_parse_and_serialize_block_templates( $query_result ) {
foreach ( $query_result as $block_template ) {
if ( 'custom' === $block_template->source ) {
continue;
}
$blocks = parse_blocks( $block_template->content );
$block_template->content = gutenberg_serialize_blocks( $blocks );
}

return $query_result;
}
add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 );

/**
* Filters the block template object after it has been (potentially) fetched from the theme file.
*
* By parsing a block template's content and then reserializing it
* via `gutenberg_serialize_blocks()`, we are able to run filters
* on the parsed blocks.
*
* @param WP_Block_Template|null $block_template The found block template, or null if there is none.
*/
function gutenberg_parse_and_serialize_blocks( $block_template ) {

$blocks = parse_blocks( $block_template->content );
$block_template->content = gutenberg_serialize_blocks( $blocks );

return $block_template;
}
add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 );

// Helper functions.
// -----------------
// The sole purpose of the following two functions (`gutenberg_serialize_block`
// and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed
// counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter
// (also called `gutenberg_serialize_block`) as an entry point for modifications
// to the parsed blocks.

/**
* Filterable version of `serialize_block()`.
*
* This function is identical to `serialize_block()`, except that it applies
* the `gutenberg_serialize_block` filter to each block before it is serialized.
*
* @param array $block The block to be serialized.
* @return string The serialized block.
*
* @see serialize_block()
*/
function gutenberg_serialize_block( $block ) {
$block_content = '';

/**
* Filters a parsed block before it is serialized.
*
* @param array $block The block to be serialized.
*/
$block = apply_filters( 'gutenberg_serialize_block', $block );

$index = 0;
foreach ( $block['innerContent'] as $chunk ) {
if ( is_string( $chunk ) ) {
$block_content .= $chunk;
} else { // Compare to WP_Block::render().
$inner_block = $block['innerBlocks'][ $index++ ];
$block_content .= gutenberg_serialize_block( $inner_block );
}
}

if ( ! is_array( $block['attrs'] ) ) {
$block['attrs'] = array();
}

return get_comment_delimited_block_content(
$block['blockName'],
$block['attrs'],
$block_content
);
}

/**
* Filterable version of `serialize_blocks()`.
*
* This function is identical to `serialize_blocks()`, except that it applies
* the `gutenberg_serialize_block` filter to each block before it is serialized.
*
* @param array $blocks The blocks to be serialized.
* @return string[] The serialized blocks.
*
* @see serialize_blocks()
*/
function gutenberg_serialize_blocks( $blocks ) {
return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) );
}
@@ -0,0 +1,40 @@
<?php
/**
* REST API: Gutenberg_REST_Block_Patterns_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Core class used to access block patterns via the REST API.
*
* @since 6.4.0
*
* @see WP_REST_Controller
*/
class Gutenberg_REST_Block_Patterns_Controller extends Gutenberg_REST_Block_Patterns_Controller_6_3 {
/**
* Prepare a raw block pattern before it gets output in a REST API response.
*
* @todo In the long run, we'd likely want to have a filter in the `WP_Block_Patterns_Registry` class
* instead to allow us plugging in code like this.
*
* @param array $item Raw pattern as registered, before any changes.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$response = parent::prepare_item_for_response( $item, $request );
if ( ! gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
return $response;
}

$data = $response->get_data();

$blocks = parse_blocks( $data['content'] );
$data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render?
return rest_ensure_response( $data );
}
}
11 changes: 11 additions & 0 deletions lib/experimental/rest-api.php
Expand Up @@ -10,6 +10,17 @@
die( 'Silence is golden.' );
}

if ( gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
/**
* Registers the block patterns REST API routes.
*/
function gutenberg_register_rest_block_patterns() {
$block_patterns = new Gutenberg_REST_Block_Patterns_Controller();
$block_patterns->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' );
}

/**
* Registers the customizer nonces REST API routes.
*/
Expand Down
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Expand Up @@ -91,6 +91,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-auto-inserting-blocks',
__( 'Auto-inserting blocks', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Test Auto-inserting blocks', 'gutenberg' ),
'id' => 'gutenberg-auto-inserting-blocks',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
6 changes: 6 additions & 0 deletions lib/load.php
Expand Up @@ -69,6 +69,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php';
}
require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php';
if ( gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
require_once __DIR__ . '/experimental/class-gutenberg-rest-block-patterns-controller.php';
}
require_once __DIR__ . '/experimental/rest-api.php';
}

Expand Down Expand Up @@ -117,6 +120,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/disable-tinymce.php';
}

if ( gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
require __DIR__ . '/experimental/auto-inserting-blocks.php';
}
require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php';
require __DIR__ . '/experimental/interactivity-api/store.php';
require __DIR__ . '/experimental/interactivity-api/scripts.php';
Expand Down
12 changes: 11 additions & 1 deletion packages/block-library/src/pattern/index.php
Expand Up @@ -41,7 +41,17 @@ function render_block_core_pattern( $attributes ) {
}

$pattern = $registry->get_registered( $slug );
return do_blocks( $pattern['content'] );
$content = $pattern['content'];

$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments && ! empty( $gutenberg_experiments['gutenberg-auto-inserting-blocks'] ) ) {
// TODO: In the long run, we'd likely want to have a filter in the `WP_Block_Patterns_Registry` class
// instead to allow us plugging in code like this.
$blocks = parse_blocks( $content );
$content = gutenberg_serialize_blocks( $blocks );
}

return do_blocks( $content );
}

add_action( 'init', 'register_block_core_pattern' );

0 comments on commit b0fb1c2

Please sign in to comment.