diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index fdd9de9afff0d..efdd9b1b1aa63 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1518,6 +1518,83 @@ function traverse_and_serialize_block( $block, $pre_callback = null, $post_callb ); } +/** + * Replaces patterns in a block tree with their content. + * + * @since 6.6.0 + * + * @param array $blocks An array blocks. + * + * @return array An array of blocks with patterns replaced by their content. + */ +function resolve_pattern_blocks( $blocks ) { + static $inner_content; + // Keep track of seen references to avoid infinite loops. + static $seen_refs = array(); + $i = 0; + while ( $i < count( $blocks ) ) { + if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) { + $attrs = $blocks[ $i ]['attrs']; + + if ( empty( $attrs['slug'] ) ) { + ++$i; + continue; + } + + $slug = $attrs['slug']; + + if ( isset( $seen_refs[ $slug ] ) ) { + // Skip recursive patterns. + array_splice( $blocks, $i, 1 ); + continue; + } + + $registry = WP_Block_Patterns_Registry::get_instance(); + $pattern = $registry->get_registered( $slug ); + + // Skip unknown patterns. + if ( ! $pattern ) { + ++$i; + continue; + } + + $blocks_to_insert = parse_blocks( $pattern['content'] ); + $seen_refs[ $slug ] = true; + $prev_inner_content = $inner_content; + $inner_content = null; + $blocks_to_insert = resolve_pattern_blocks( $blocks_to_insert ); + $inner_content = $prev_inner_content; + unset( $seen_refs[ $slug ] ); + array_splice( $blocks, $i, 1, $blocks_to_insert ); + + // If we have inner content, we need to insert nulls in the + // inner content array, otherwise serialize_blocks will skip + // blocks. + if ( $inner_content ) { + $null_indices = array_keys( $inner_content, null, true ); + $content_index = $null_indices[ $i ]; + $nulls = array_fill( 0, count( $blocks_to_insert ), null ); + array_splice( $inner_content, $content_index, 1, $nulls ); + } + + // Skip inserted blocks. + $i += count( $blocks_to_insert ); + } else { + if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) { + $prev_inner_content = $inner_content; + $inner_content = $blocks[ $i ]['innerContent']; + $blocks[ $i ]['innerBlocks'] = resolve_pattern_blocks( + $blocks[ $i ]['innerBlocks'] + ); + $blocks[ $i ]['innerContent'] = $inner_content; + $inner_content = $prev_inner_content; + } + ++$i; + } + } + return $blocks; +} + /** * Given an array of parsed block trees, applies callbacks before and after serializing them and * returns their concatenated output. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php index d8f083924e030..c98b2a7c57c5e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php @@ -162,6 +162,12 @@ protected function migrate_pattern_categories( $pattern ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function prepare_item_for_response( $item, $request ) { + // Resolve pattern blocks so they don't need to be resolved client-side + // in the editor, improving performance. + $blocks = parse_blocks( $item['content'] ); + $blocks = resolve_pattern_blocks( $blocks ); + $item['content'] = serialize_blocks( $blocks ); + $fields = $this->get_fields_for_response( $request ); $keys = array( 'name' => 'name', diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 0ac6c7de5a22e..cbf0ee040a9b0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -668,6 +668,12 @@ protected function prepare_item_for_database( $request ) { * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { + // Resolve pattern blocks so they don't need to be resolved client-side + // in the editor, improving performance. + $blocks = parse_blocks( $item->content ); + $blocks = resolve_pattern_blocks( $blocks ); + $item->content = serialize_blocks( $blocks ); + // Restores the more descriptive, specific name for use within this method. $template = $item; diff --git a/tests/phpunit/tests/blocks/resolvePatternBlocks.php b/tests/phpunit/tests/blocks/resolvePatternBlocks.php new file mode 100644 index 0000000000000..b2e6fa6463f7d --- /dev/null +++ b/tests/phpunit/tests/blocks/resolvePatternBlocks.php @@ -0,0 +1,72 @@ + 'Test', + 'content' => 'HelloWorld', + 'description' => 'Test pattern.', + ) + ); + register_block_pattern( + 'core/recursive', + array( + 'title' => 'Recursive', + 'content' => 'Recursive', + 'description' => 'Recursive pattern.', + ) + ); + } + + public function tear_down() { + unregister_block_pattern( 'core/test' ); + unregister_block_pattern( 'core/recursive' ); + + parent::tear_down(); + } + + /** + * @dataProvider data_should_resolve_pattern_blocks_as_expected + * + * @ticket 61228 + * + * @param string $blocks A string representing blocks that need resolving. + * @param string $expected Expected result. + */ + public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expected ) { + $actual = resolve_pattern_blocks( parse_blocks( $blocks ) ); + $this->assertSame( $expected, serialize_blocks( $actual ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_resolve_pattern_blocks_as_expected() { + return array( + // Works without attributes, leaves the block as is. + 'pattern with no slug attribute' => array( '', '' ), + // Resolves the pattern. + 'test pattern' => array( '', 'HelloWorld' ), + // Skips recursive patterns. + 'recursive pattern' => array( '', 'Recursive' ), + // Resolves the pattern within a block. + 'pattern within a block' => array( 'BeforeAfter', 'BeforeHelloWorldAfter' ), + ); + } +}