Skip to content

Commit 774808a

Browse files
authored
Merge pull request #87 from Automattic/fix/synced-pattern-nesting
Correctly parse synced patterns with nested blocks
2 parents be82f1f + fc24575 commit 774808a

File tree

4 files changed

+473
-228
lines changed

4 files changed

+473
-228
lines changed

.github/workflows/phpcs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929

3030
- name: Cache Composer packages
3131
id: composer-cache
32-
uses: actions/cache@v2
32+
uses: actions/cache@v4
3333
with:
3434
path: vendor
3535
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}

src/parser/block-additions/core-block.php

Lines changed: 34 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
defined( 'ABSPATH' ) || die();
1111

1212
use WP_Block;
13-
use WP_Block_Supports;
14-
use function add_action;
13+
use WP_Post;
14+
use WPCOMVIP\BlockDataApi\ContentParser;
15+
1516
use function add_filter;
16-
use function remove_filter;
17+
use function get_post;
18+
use function parse_blocks;
1719

1820
/**
1921
* Enhance core/block block by capturing its inner blocks.
@@ -26,59 +28,22 @@ class CoreBlock {
2628
*/
2729
private static $block_name = 'core/block';
2830

29-
/**
30-
* A store of captured inner blocks. See `capture_inner_blocks`.
31-
*
32-
* @var array
33-
*
34-
* @access private
35-
*/
36-
protected static $captured_inner_blocks = [];
37-
3831
/**
3932
* Initialize the CoreBlock class.
4033
*
4134
* @access private
4235
*/
4336
public static function init(): void {
44-
add_action( 'vip_block_data_api__before_block_render', [ __CLASS__, 'setup_before_render' ], 10, 0 );
45-
add_action( 'vip_block_data_api__after_block_render', [ __CLASS__, 'cleanup_after_render' ], 10, 0 );
46-
add_filter( 'vip_block_data_api__sourced_block_inner_blocks', [ __CLASS__, 'get_inner_blocks' ], 5, 4 );
37+
add_filter( 'vip_block_data_api__sourced_block_inner_blocks', [ __CLASS__, 'get_inner_blocks' ], 5, 5 );
4738
add_filter( 'vip_block_data_api__sourced_block_result', [ __CLASS__, 'remove_content_array' ], 5, 2 );
4839
}
4940

5041
/**
51-
* Setup before render.
52-
*/
53-
public static function setup_before_render(): void {
54-
/**
55-
* Hook into the `render_block` filter, which is near the end of WP_Block#render().
56-
* This allows us to capture the inner blocks of synced patterns ("core/block").
57-
* See `capture_inner_blocks`.
58-
*/
59-
add_filter( 'render_block', [ __CLASS__, 'capture_inner_blocks' ], 10, 3 );
60-
}
61-
62-
/**
63-
* Cleanup after render.
64-
*/
65-
public static function cleanup_after_render() {
66-
self::$captured_inner_blocks = [];
67-
remove_filter( 'render_block', [ __CLASS__, 'capture_inner_blocks' ], 10 );
68-
}
69-
70-
/**
71-
* Capture the inner blocks of synced patterns during block rendering. Intended
72-
* for use with the `render_block` filter.
73-
*
74-
* We have no intention of filtering the rendered block content, but this hook
75-
* is conveniently located near the end of WP_Block#render() after block
76-
* processing is finished. We get access to the parent block via the global
77-
* static class `WP_Block_Supports`.
42+
* Get the inner blocks of a synced pattern / reusable block. Intended for use
43+
* with the `vip_block_data_api__sourced_block_inner_blocks` filter.
7844
*
79-
* This approach is necessary because synced patterns (core/block) are dynamic
80-
* blocks, and core's method of rendering dynamic blocks severs the connection
81-
* between the parent block and its inner blocks:
45+
* Synced patterns are dynamic blocks, and core's method of rendering dynamic
46+
* blocks severs the connection between the parent block and its inner blocks:
8247
*
8348
* https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/class-wp-block.php#L519
8449
*
@@ -88,105 +53,47 @@ public static function cleanup_after_render() {
8853
* missing from the Block Data API. Capturing synced pattern content as inner
8954
* blocks is extremely useful and avoids the need for additional API calls.
9055
*
91-
* @param string $block_content Rendered block content.
92-
* @param array $parsed_block Parsed block data.
93-
* @param WP_Block $block Block instance.
94-
* @return string
95-
*/
96-
public static function capture_inner_blocks( string $block_content, array $parsed_block, WP_Block $block ): string {
97-
// If this block is a synced pattern, that means it is finished rendering.
98-
// Lock its inner blocks to prevent further captures in case it is rendered
99-
// elsewhere in the tree.
100-
if ( self::$block_name === $block->name ) {
101-
$store_key = self::get_store_key( $parsed_block );
102-
if ( isset( self::$captured_inner_blocks[ $store_key ] ) ) {
103-
self::$captured_inner_blocks[ $store_key ]['locked'] = true;
104-
}
105-
}
106-
107-
// Get the parent block that is currently being rendered. This is fragile,
108-
// but is currently the only way we can get access to the parent block from
109-
// inside a dynamic block's render callback function.
110-
//
111-
// https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/class-wp-block.php#L517
112-
$parent_block = isset( WP_Block_Supports::$block_to_render ) ? WP_Block_Supports::$block_to_render : [];
113-
114-
// If the parent block is not a synced pattern, do nothing.
115-
if ( ! isset( $parent_block['attrs']['ref'] ) || self::$block_name !== $parent_block['blockName'] ) {
116-
return $block_content;
117-
}
118-
119-
// Capture the inner block for this synced pattern.
120-
self::capture_inner_block( $parent_block, $block );
121-
122-
return $block_content;
123-
}
124-
125-
/**
126-
* Get captured inner blocks for synced patterns. Intended for use with
127-
* the `vip_block_data_api__sourced_block_inner_blocks` filter.
56+
* This requires us to reimplement some logic from `render_block_core_block()`:
57+
*
58+
* https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L19
12859
*
12960
* @param array $inner_blocks Inner blocks.
13061
* @param string $block_name Block name.
131-
* @param int|null $_post_id Post ID (unused).
62+
* @param int|null $post_id Post ID.
13263
* @param array $parsed_block Parsed block data.
13364
* @return array
13465
*/
135-
public static function get_inner_blocks( array $inner_blocks, string $block_name, int|null $_post_id, array $parsed_block ): array {
66+
public static function get_inner_blocks( array $inner_blocks, string $block_name, int|null $post_id, array $parsed_block ): array {
67+
// Not a synced pattern? Return the inner blocks unchanged.
13668
if ( self::$block_name !== $block_name || ! isset( $parsed_block['attrs']['ref'] ) ) {
13769
return $inner_blocks;
13870
}
13971

140-
$store_key = self::get_store_key( $parsed_block );
72+
$context = [];
14173

142-
if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) {
143-
return $inner_blocks;
74+
// Support synced pattern overrides. Copied and adapted from core:
75+
// https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L81
76+
//
77+
// In our case, we don't need to filter the context since we can pass it in.
78+
if ( isset( $parsed_block['attrs']['content'] ) ) {
79+
$context['pattern/overrides'] = $parsed_block['attrs']['content'];
14480
}
14581

146-
return self::$captured_inner_blocks[ $store_key ]['inner_blocks'];
147-
}
82+
// Load, parse, and render the inner blocks of the synced pattern, passing
83+
// along its block context. We intentionally do not recursively call
84+
// ContentParser->parse() to avoid calling telemetry and filters again.
85+
$parser = new ContentParser();
86+
$post = get_post( $parsed_block['attrs']['ref'] );
14887

149-
/**
150-
* Create a unique key that can be used to identify a synced pattern. This
151-
* allows us to store and retrieve inner blocks for synced patterns and avoid
152-
* duplication when they are used multiple times within the same tree.
153-
*
154-
* Using a hash of attributes is important because they may contain synced
155-
* pattern overrides, which can change the inner block content. The attributes
156-
* contain the synced pattern post ID, so uniqueness is built-in.
157-
*
158-
* @param array $parsed_block Parsed block data.
159-
* @return string
160-
*/
161-
protected static function get_store_key( array $parsed_block ): string {
162-
// Include the synced pattern ID in the key just for legibility.
163-
$synced_pattern_id = $parsed_block['attrs']['ref'] ?? null;
164-
$attribute_json = wp_json_encode( $parsed_block['attrs'] );
165-
166-
return sprintf( '%s_%s', strval( $synced_pattern_id ), sha1( $attribute_json ) );
167-
}
168-
169-
/**
170-
* Capture inner block for a synced pattern.
171-
*
172-
* @param array $synced_pattern Synced pattern block (parsed block).
173-
* @param WP_Block $block Inner block.
174-
*/
175-
protected static function capture_inner_block( array $synced_pattern, WP_Block $block ): void {
176-
$store_key = self::get_store_key( $synced_pattern );
177-
if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) {
178-
self::$captured_inner_blocks[ $store_key ] = [
179-
'inner_blocks' => [],
180-
'locked' => false,
181-
];
88+
if ( ! $post instanceof WP_Post ) {
89+
return [];
18290
}
18391

184-
// This pattern has already been rendered somewhere in the tree and is now locked.
185-
if ( self::$captured_inner_blocks[ $store_key ]['locked'] ) {
186-
return;
187-
}
92+
$blocks = parse_blocks( $post->post_content );
18893

189-
self::$captured_inner_blocks[ $store_key ]['inner_blocks'][] = $block;
94+
return array_map( function ( array $block ) use ( $parser, $context, $post_id ): WP_Block {
95+
return $parser->render_parsed_block( $block, $post_id, $context );
96+
}, $blocks );
19097
}
19198

19299
/**

src/parser/content-parser.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) {
180180
*/
181181
do_action( 'vip_block_data_api__before_block_render', $blocks, $post_id );
182182

183-
$sourced_blocks = array_map( function ( $block ) use ( $filter_options ) {
183+
$sourced_blocks = array_map( function ( $block ) use ( $filter_options, $post_id ) {
184184
// Render the block, then walk the tree using source_block to apply our
185185
// sourced attribute logic.
186-
$rendered_block = $this->render_parsed_block( $block );
186+
$rendered_block = $this->render_parsed_block( $block, $post_id );
187187

188188
return $this->source_block( $rendered_block, $filter_options );
189189
}, $blocks );
@@ -252,14 +252,15 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) {
252252
*
253253
* https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks.php#L1959
254254
*
255-
* @param array $parsed_block Parsed block (result of `parse_blocks`).
255+
* @param array $parsed_block Parsed block (result of `parse_blocks`).
256+
* @param int|null $post_id Post ID.
257+
* @param array $context Context to be passed to the block.
256258
* @return WP_Block
257259
*/
258-
protected function render_parsed_block( array $parsed_block ): WP_Block {
259-
$context = [];
260-
if ( is_int( $this->post_id ) ) {
261-
$context['postId'] = $this->post_id;
262-
$context['postType'] = get_post_type( $this->post_id );
260+
public function render_parsed_block( array $parsed_block, int|null $post_id, array $context = [] ): WP_Block {
261+
if ( is_int( $post_id ) ) {
262+
$context['postId'] = $post_id;
263+
$context['postType'] = get_post_type( $post_id );
263264
}
264265

265266
$context = apply_filters( 'render_block_context', $context, $parsed_block, null );
@@ -280,7 +281,7 @@ protected function render_parsed_block( array $parsed_block ): WP_Block {
280281
*
281282
* @access private
282283
*/
283-
protected function source_block( WP_Block $block, array $filter_options ) {
284+
protected function source_block( WP_Block $block, array $filter_options ): array|null {
284285
$block_name = $block->name;
285286

286287
if ( ! $this->should_block_be_included( $block, $filter_options ) ) {
@@ -309,7 +310,7 @@ protected function source_block( WP_Block $block, array $filter_options ) {
309310
* @param array $inner_blocks An array of inner block (WP_Block) instances.
310311
* @param string $block_name Name of the parsed block, e.g. 'core/paragraph'.
311312
* @param int $post_id Post ID associated with the parsed block.
312-
* @param array $block Result of parse_blocks() for this block.
313+
* @param array $parsed_block Result of parse_blocks() for this block.
313314
*/
314315
$inner_blocks = apply_filters( 'vip_block_data_api__sourced_block_inner_blocks', $inner_blocks, $block_name, $this->post_id, $block->parsed_block );
315316

0 commit comments

Comments
 (0)