diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index ecb8f52392fef9..d7fa31cd33fe6a 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -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. diff --git a/lib/experimental/auto-inserting-blocks.php b/lib/experimental/auto-inserting-blocks.php new file mode 100644 index 00000000000000..9df94fcbfcd8fb --- /dev/null +++ b/lib/experimental/auto-inserting-blocks.php @@ -0,0 +1,246 @@ + 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 ) ); +} diff --git a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php b/lib/experimental/class-gutenberg-rest-block-patterns-controller.php new file mode 100644 index 00000000000000..1ac567959b146f --- /dev/null +++ b/lib/experimental/class-gutenberg-rest-block-patterns-controller.php @@ -0,0 +1,40 @@ +get_data(); + + $blocks = parse_blocks( $data['content'] ); + $data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render? + + return rest_ensure_response( $data ); + } +} diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index 7c6a9bf74d7395..8e548600b3875e 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -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. */ diff --git a/lib/experiments-page.php b/lib/experiments-page.php index ce30242d20f81d..3f468d0cbd12db 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -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' diff --git a/lib/load.php b/lib/load.php index 71be1068b7061b..85e9f9575e6e6f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -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'; } @@ -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'; diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 6368bdb7b74875..bc42e891d9f1fd 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -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' );