From e4b69afb9b598ca42fd13e0720eca7227062a959 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 11:15:14 -0400 Subject: [PATCH 1/7] Add failing tests --- tests/parser/test-synced-patterns.php | 495 +++++++++++++++++++++----- 1 file changed, 405 insertions(+), 90 deletions(-) diff --git a/tests/parser/test-synced-patterns.php b/tests/parser/test-synced-patterns.php index 8379a68..ee58026 100644 --- a/tests/parser/test-synced-patterns.php +++ b/tests/parser/test-synced-patterns.php @@ -226,6 +226,273 @@ public function test_synced_pattern_with_override() { $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in synced pattern' ); } + /* Synced pattern with deeply nested content */ + + public function test_synced_pattern_with_deeply_nested_content() { + $synced_pattern_content = ' + +
+ +
+ +

Nested synced pattern content

+ + +
+ +
+ +

Deeply nested synced pattern content

+ +
+ +
+ +
+ +
+ + '; + + $synced_pattern = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $html = sprintf( '', $synced_pattern->ID ); + + $expected_blocks = [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Nested synced pattern content', + ], + ], + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Deeply nested synced pattern content', + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in synced pattern' ); + } + + /* Synced pattern with sourced attributen nested content */ + + public function test_synced_pattern_with_sourced_attribute_in_nested_content() { + $this->register_block_with_attributes( 'test/custom-block', [ + 'content' => [ + 'type' => 'rich-text', + 'source' => 'rich-text', + 'selector' => 'p', + '__experimentalRole' => 'content', + ], + 'bing' => [ + 'type' => 'string', + 'source' => 'attribute', + 'selector' => 'p', + 'attribute' => 'data-bing', + ], + ] ); + + $synced_pattern_content = ' + +
+ +
+ +

My nested synced pattern content

+ +
+ +
+ + '; + + $synced_pattern = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $html = sprintf( '', $synced_pattern->ID ); + + $expected_blocks = [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My nested synced pattern content', + 'bing' => 'bong', + ], + ], + ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'], sprintf( 'Blocks not equal: %s', wp_json_encode( $blocks['blocks'] ) ) ); + } + + /* Synced pattern with override in nested content */ + + public function test_synced_pattern_with_override_in_nested_content() { + $synced_pattern_content = ' + +
+ +
+ +

Default content

+ +
+ +
+ + '; + + $synced_pattern = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $html = sprintf( ' + + ', $synced_pattern->ID ); + + $expected_blocks = [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Overridden content', // Overridden by synced pattern override + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', + ], + ], + ], + ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in synced pattern' ); + } + /* Multiple nested synced patterns with block bindings -- FINAL BOSS! */ public function test_multiple_nested_synced_patterns_with_block_bindings() { @@ -254,6 +521,9 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { ] ); $synced_pattern_content_1 = ' + +
+

My first synced pattern content

@@ -265,6 +535,9 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() {

Default content

+ +
+ '; $synced_pattern_1 = $this->factory()->post->create_and_get( [ @@ -274,6 +547,9 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { ] ); $synced_pattern_content_2 = sprintf( ' + +
+

My second synced pattern content which contains the first

@@ -283,6 +559,9 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() {

Another block to "wrap" the nested pattern

+ +
+ ', $synced_pattern_1->ID ); $synced_pattern_2 = $this->factory()->post->create_and_get( [ @@ -340,37 +619,45 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { ], 'innerBlocks' => [ [ - 'name' => 'test/custom-block', - 'attributes' => [ - 'content' => 'My first synced pattern content', - 'bing' => 'bong', + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', ], - ], - [ - 'name' => 'core/paragraph', - 'attributes' => [ - 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'test/synced-pattern-block-binding', - 'args' => [ 'foo' => 'bar' ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], ], ], ], - ], - ], - [ - 'name' => 'core/paragraph', - 'attributes' => [ - 'content' => 'Default content', - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'core/pattern-overrides', + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Default content', + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', ], ], - 'name' => 'my-override', ], ], ], @@ -391,37 +678,45 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { ], 'innerBlocks' => [ [ - 'name' => 'test/custom-block', - 'attributes' => [ - 'content' => 'My first synced pattern content', - 'bing' => 'bong', + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', ], - ], - [ - 'name' => 'core/paragraph', - 'attributes' => [ - 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'test/synced-pattern-block-binding', - 'args' => [ 'foo' => 'bar' ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], ], ], ], - ], - ], - [ - 'name' => 'core/paragraph', - 'attributes' => [ - 'content' => 'Overridden content', // Overridden by synced pattern override - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'core/pattern-overrides', + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Overridden content', // Overridden by synced pattern override + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', ], ], - 'name' => 'my-override', ], ], ], @@ -442,62 +737,78 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { ], 'innerBlocks' => [ [ - 'name' => 'test/custom-block', - 'attributes' => [ - 'content' => 'My second synced pattern content which contains the first', - 'bing' => 'bang', - ], - ], - [ - 'name' => 'core/block', + 'name' => 'core/group', 'attributes' => [ - 'ref' => $synced_pattern_1->ID, + 'tagName' => 'div', ], 'innerBlocks' => [ [ 'name' => 'test/custom-block', 'attributes' => [ - 'content' => 'My first synced pattern content', - 'bing' => 'bong', + 'content' => 'My second synced pattern content which contains the first', + 'bing' => 'bang', ], ], [ - 'name' => 'core/paragraph', - 'attributes' => [ - 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'test/synced-pattern-block-binding', - 'args' => [ 'foo' => 'bar' ], + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern_1->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'core/group', + 'attributes' => [ + 'tagName' => 'div', + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Default content', + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', + ], + ], ], ], ], ], ], [ - 'name' => 'core/paragraph', + 'name' => 'test/custom-block', 'attributes' => [ - 'content' => 'Default content', - 'metadata' => [ - 'bindings' => [ - 'content' => [ - 'source' => 'core/pattern-overrides', - ], - ], - 'name' => 'my-override', - ], + 'content' => 'Another block to "wrap" the nested pattern', + 'bing' => 'bang', ], ], ], ], - [ - 'name' => 'test/custom-block', - 'attributes' => [ - 'content' => 'Another block to "wrap" the nested pattern', - 'bing' => 'bang', - ], - ], ], ], ], @@ -519,15 +830,19 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { // First synced pattern $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in first container block' ); - $this->assertEquals( 3, count( $blocks['blocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + $this->assertEquals( 3, count( $blocks['blocks'][0]['innerBlocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern group block' ); // First synced pattern, repeated (contains pattern override) $this->assertEquals( 1, count( $blocks['blocks'][1]['innerBlocks'] ), 'Too many inner blocks in first container block' ); - $this->assertEquals( 3, count( $blocks['blocks'][1]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + $this->assertEquals( 1, count( $blocks['blocks'][1]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + $this->assertEquals( 3, count( $blocks['blocks'][1]['innerBlocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern group block' ); // Second synced pattern $this->assertEquals( 1, count( $blocks['blocks'][2]['innerBlocks'] ), 'Too many inner blocks in second container block' ); - $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in second synced pattern' ); - $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'][1]['innerBlocks'] ), 'Too many inner blocks in nested pattern in second synced pattern' ); + $this->assertEquals( 1, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in second synced pattern' ); + $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in second synced pattern group block' ); + $this->assertEquals( 1, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'][0]['innerBlocks'][1]['innerBlocks'] ), 'Too many inner blocks in nested pattern in second synced pattern' ); + $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'][0]['innerBlocks'][1]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in nested pattern in second synced pattern group block' ); } } From 91a935015f967a3a3794ba20f90869dc341dedce Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 11:16:27 -0400 Subject: [PATCH 2/7] Parse synced patterns and render manually --- src/parser/block-additions/core-block.php | 161 +++++----------------- src/parser/content-parser.php | 21 +-- 2 files changed, 42 insertions(+), 140 deletions(-) diff --git a/src/parser/block-additions/core-block.php b/src/parser/block-additions/core-block.php index 14d5092..1e7290b 100644 --- a/src/parser/block-additions/core-block.php +++ b/src/parser/block-additions/core-block.php @@ -10,10 +10,11 @@ defined( 'ABSPATH' ) || die(); use WP_Block; -use WP_Block_Supports; -use function add_action; +use WPCOMVIP\BlockDataApi\ContentParser; + use function add_filter; -use function remove_filter; +use function get_post; +use function parse_blocks; /** * Enhance core/block block by capturing its inner blocks. @@ -26,59 +27,22 @@ class CoreBlock { */ private static $block_name = 'core/block'; - /** - * A store of captured inner blocks. See `capture_inner_blocks`. - * - * @var array - * - * @access private - */ - protected static $captured_inner_blocks = []; - /** * Initialize the CoreBlock class. * * @access private */ public static function init(): void { - add_action( 'vip_block_data_api__before_block_render', [ __CLASS__, 'setup_before_render' ], 10, 0 ); - add_action( 'vip_block_data_api__after_block_render', [ __CLASS__, 'cleanup_after_render' ], 10, 0 ); - add_filter( 'vip_block_data_api__sourced_block_inner_blocks', [ __CLASS__, 'get_inner_blocks' ], 5, 4 ); + add_filter( 'vip_block_data_api__sourced_block_inner_blocks', [ __CLASS__, 'get_inner_blocks' ], 5, 5 ); add_filter( 'vip_block_data_api__sourced_block_result', [ __CLASS__, 'remove_content_array' ], 5, 2 ); } /** - * Setup before render. - */ - public static function setup_before_render(): void { - /** - * Hook into the `render_block` filter, which is near the end of WP_Block#render(). - * This allows us to capture the inner blocks of synced patterns ("core/block"). - * See `capture_inner_blocks`. - */ - add_filter( 'render_block', [ __CLASS__, 'capture_inner_blocks' ], 10, 3 ); - } - - /** - * Cleanup after render. - */ - public static function cleanup_after_render() { - self::$captured_inner_blocks = []; - remove_filter( 'render_block', [ __CLASS__, 'capture_inner_blocks' ], 10 ); - } - - /** - * Capture the inner blocks of synced patterns during block rendering. Intended - * for use with the `render_block` filter. + * Get the inner blocks of a synced pattern / reusable block. Intended for use + * with the `vip_block_data_api__sourced_block_inner_blocks` filter. * - * We have no intention of filtering the rendered block content, but this hook - * is conveniently located near the end of WP_Block#render() after block - * processing is finished. We get access to the parent block via the global - * static class `WP_Block_Supports`. - * - * This approach is necessary because synced patterns (core/block) are dynamic - * blocks, and core's method of rendering dynamic blocks severs the connection - * between the parent block and its inner blocks: + * Synced patterns are dynamic blocks, and core's method of rendering dynamic + * blocks severs the connection between the parent block and its inner blocks: * * https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/class-wp-block.php#L519 * @@ -88,105 +52,42 @@ public static function cleanup_after_render() { * missing from the Block Data API. Capturing synced pattern content as inner * blocks is extremely useful and avoids the need for additional API calls. * - * @param string $block_content Rendered block content. - * @param array $parsed_block Parsed block data. - * @param WP_Block $block Block instance. - * @return string - */ - public static function capture_inner_blocks( string $block_content, array $parsed_block, WP_Block $block ): string { - // If this block is a synced pattern, that means it is finished rendering. - // Lock its inner blocks to prevent further captures in case it is rendered - // elsewhere in the tree. - if ( self::$block_name === $block->name ) { - $store_key = self::get_store_key( $parsed_block ); - if ( isset( self::$captured_inner_blocks[ $store_key ] ) ) { - self::$captured_inner_blocks[ $store_key ]['locked'] = true; - } - } - - // Get the parent block that is currently being rendered. This is fragile, - // but is currently the only way we can get access to the parent block from - // inside a dynamic block's render callback function. - // - // https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/class-wp-block.php#L517 - $parent_block = isset( WP_Block_Supports::$block_to_render ) ? WP_Block_Supports::$block_to_render : []; - - // If the parent block is not a synced pattern, do nothing. - if ( ! isset( $parent_block['attrs']['ref'] ) || self::$block_name !== $parent_block['blockName'] ) { - return $block_content; - } - - // Capture the inner block for this synced pattern. - self::capture_inner_block( $parent_block, $block ); - - return $block_content; - } - - /** - * Get captured inner blocks for synced patterns. Intended for use with - * the `vip_block_data_api__sourced_block_inner_blocks` filter. + * This requires us to reimplement some logic from `render_block_core_block()`: + * + * https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L19 * * @param array $inner_blocks Inner blocks. * @param string $block_name Block name. - * @param int|null $_post_id Post ID (unused). + * @param int|null $post_id Post ID. * @param array $parsed_block Parsed block data. * @return array */ - public static function get_inner_blocks( array $inner_blocks, string $block_name, int|null $_post_id, array $parsed_block ): array { + public static function get_inner_blocks( array $inner_blocks, string $block_name, int|null $post_id, array $parsed_block ): array { + // Not a synced pattern? Return the inner blocks unchanged. if ( self::$block_name !== $block_name || ! isset( $parsed_block['attrs']['ref'] ) ) { return $inner_blocks; } - $store_key = self::get_store_key( $parsed_block ); - - if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) { - return $inner_blocks; - } - - return self::$captured_inner_blocks[ $store_key ]['inner_blocks']; - } - - /** - * Create a unique key that can be used to identify a synced pattern. This - * allows us to store and retrieve inner blocks for synced patterns and avoid - * duplication when they are used multiple times within the same tree. - * - * Using a hash of attributes is important because they may contain synced - * pattern overrides, which can change the inner block content. The attributes - * contain the synced pattern post ID, so uniqueness is built-in. - * - * @param array $parsed_block Parsed block data. - * @return string - */ - protected static function get_store_key( array $parsed_block ): string { - // Include the synced pattern ID in the key just for legibility. - $synced_pattern_id = $parsed_block['attrs']['ref'] ?? null; - $attribute_json = wp_json_encode( $parsed_block['attrs'] ); - - return sprintf( '%s_%s', strval( $synced_pattern_id ), sha1( $attribute_json ) ); - } + $context = []; - /** - * Capture inner block for a synced pattern. - * - * @param array $synced_pattern Synced pattern block (parsed block). - * @param WP_Block $block Inner block. - */ - protected static function capture_inner_block( array $synced_pattern, WP_Block $block ): void { - $store_key = self::get_store_key( $synced_pattern ); - if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) { - self::$captured_inner_blocks[ $store_key ] = [ - 'inner_blocks' => [], - 'locked' => false, - ]; + // Support synced pattern overrides. Copied and adapted from core: + // https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L81 + // + // In our case, we don't need to filter the context since we can pass it in. + if ( isset( $parsed_block['attrs']['content'] ) ) { + $context['pattern/overrides'] = $parsed_block['attrs']['content']; } - // This pattern has already been rendered somewhere in the tree and is now locked. - if ( self::$captured_inner_blocks[ $store_key ]['locked'] ) { - return; - } + // Load, parse, and render the inner blocks of the synced pattern, passing + // along its block context. We intentionally do not recursively call + // ContentParser->parse() to avoid calling telemetry and filters again. + $parser = new ContentParser(); + $post = get_post( $parsed_block['attrs']['ref'] ); + $blocks = parse_blocks( $post->post_content ); - self::$captured_inner_blocks[ $store_key ]['inner_blocks'][] = $block; + return array_map( function ( array $block ) use ( $parser, $context, $post_id ): WP_Block { + return $parser->render_parsed_block( $block, $post_id, $context ); + }, $blocks ); } /** diff --git a/src/parser/content-parser.php b/src/parser/content-parser.php index 7c8535b..3ddf068 100644 --- a/src/parser/content-parser.php +++ b/src/parser/content-parser.php @@ -180,10 +180,10 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { */ do_action( 'vip_block_data_api__before_block_render', $blocks, $post_id ); - $sourced_blocks = array_map( function ( $block ) use ( $filter_options ) { + $sourced_blocks = array_map( function ( $block ) use ( $filter_options, $post_id ) { // Render the block, then walk the tree using source_block to apply our // sourced attribute logic. - $rendered_block = $this->render_parsed_block( $block ); + $rendered_block = $this->render_parsed_block( $block, $post_id ); return $this->source_block( $rendered_block, $filter_options ); }, $blocks ); @@ -252,14 +252,15 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { * * https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks.php#L1959 * - * @param array $parsed_block Parsed block (result of `parse_blocks`). + * @param array $parsed_block Parsed block (result of `parse_blocks`). + * @param int|null $post_id Post ID. + * @param array $context Context to be passed to the block. * @return WP_Block */ - protected function render_parsed_block( array $parsed_block ): WP_Block { - $context = []; - if ( is_int( $this->post_id ) ) { - $context['postId'] = $this->post_id; - $context['postType'] = get_post_type( $this->post_id ); + public function render_parsed_block( array $parsed_block, int|null $post_id, array $context = [] ): WP_Block { + if ( is_int( $post_id ) ) { + $context['postId'] = $post_id; + $context['postType'] = get_post_type( $post_id ); } $context = apply_filters( 'render_block_context', $context, $parsed_block, null ); @@ -280,7 +281,7 @@ protected function render_parsed_block( array $parsed_block ): WP_Block { * * @access private */ - protected function source_block( WP_Block $block, array $filter_options ) { + protected function source_block( WP_Block $block, array $filter_options ): array|null { $block_name = $block->name; if ( ! $this->should_block_be_included( $block, $filter_options ) ) { @@ -309,7 +310,7 @@ protected function source_block( WP_Block $block, array $filter_options ) { * @param array $inner_blocks An array of inner block (WP_Block) instances. * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. * @param int $post_id Post ID associated with the parsed block. - * @param array $block Result of parse_blocks() for this block. + * @param array $parsed_block Result of parse_blocks() for this block. */ $inner_blocks = apply_filters( 'vip_block_data_api__sourced_block_inner_blocks', $inner_blocks, $block_name, $this->post_id, $block->parsed_block ); From acb4d50fc575ff2f428efc72ad8b6b0db09f6503 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 12:08:46 -0400 Subject: [PATCH 3/7] Update cache GitHub action --- .github/workflows/phpcs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index 746e22c..aca92bc 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -29,7 +29,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} From 5dbe30812dd750bfd8414e184b97c06dff687776 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 13:23:01 -0400 Subject: [PATCH 4/7] Fix typos in test cases --- tests/parser/test-synced-patterns.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/parser/test-synced-patterns.php b/tests/parser/test-synced-patterns.php index ee58026..673d3d6 100644 --- a/tests/parser/test-synced-patterns.php +++ b/tests/parser/test-synced-patterns.php @@ -329,7 +329,7 @@ public function test_synced_pattern_with_deeply_nested_content() { $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in synced pattern' ); } - /* Synced pattern with sourced attributen nested content */ + /* Synced pattern with sourced attribute in nested content */ public function test_synced_pattern_with_sourced_attribute_in_nested_content() { $this->register_block_with_attributes( 'test/custom-block', [ @@ -415,7 +415,7 @@ public function test_synced_pattern_with_sourced_attribute_in_nested_content() { public function test_synced_pattern_with_override_in_nested_content() { $synced_pattern_content = ' -
+
@@ -522,7 +522,7 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { $synced_pattern_content_1 = ' -
+

My first synced pattern content

@@ -548,7 +548,7 @@ public function test_multiple_nested_synced_patterns_with_block_bindings() { $synced_pattern_content_2 = sprintf( ' -
+

My second synced pattern content which contains the first

From 7a84ab5c6a3e49d82c286cdffd91bafdb44dbb44 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 13:25:02 -0400 Subject: [PATCH 5/7] Add guard against missing post --- src/parser/block-additions/core-block.php | 5 +++++ tests/parser/test-synced-patterns.php | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/parser/block-additions/core-block.php b/src/parser/block-additions/core-block.php index 1e7290b..2e259cf 100644 --- a/src/parser/block-additions/core-block.php +++ b/src/parser/block-additions/core-block.php @@ -83,6 +83,11 @@ public static function get_inner_blocks( array $inner_blocks, string $block_name // ContentParser->parse() to avoid calling telemetry and filters again. $parser = new ContentParser(); $post = get_post( $parsed_block['attrs']['ref'] ); + + if ( ! $post instanceof \WP_Post ) { + return []; + } + $blocks = parse_blocks( $post->post_content ); return array_map( function ( array $block ) use ( $parser, $context, $post_id ): WP_Block { diff --git a/tests/parser/test-synced-patterns.php b/tests/parser/test-synced-patterns.php index 673d3d6..fd5efbd 100644 --- a/tests/parser/test-synced-patterns.php +++ b/tests/parser/test-synced-patterns.php @@ -165,6 +165,28 @@ public function test_multiple_synced_patterns() { $this->assertEquals( $expected_blocks, $blocks['blocks'], sprintf( 'Blocks not equal: %s', wp_json_encode( $blocks['blocks'] ) ) ); } + /* Missing synced pattern */ + + public function test_missing_synced_pattern() { + $html = sprintf( '', -1 ); + + $expected_blocks = [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => -1, + ], + // inner_blocks is omitted when empty for backwards compatibility with earlier release + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'], sprintf( 'Blocks not equal: %s', wp_json_encode( $blocks['blocks'] ) ) ); + } + /* Synced pattern with override */ public function test_synced_pattern_with_override() { From 01c2a36b5f6ebbcc0826d3800dc02bb5fb00e5ed Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 13:28:39 -0400 Subject: [PATCH 6/7] Fix alignment issue --- tests/parser/test-synced-patterns.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/parser/test-synced-patterns.php b/tests/parser/test-synced-patterns.php index fd5efbd..9636551 100644 --- a/tests/parser/test-synced-patterns.php +++ b/tests/parser/test-synced-patterns.php @@ -172,8 +172,8 @@ public function test_missing_synced_pattern() { $expected_blocks = [ [ - 'name' => 'core/block', - 'attributes' => [ + 'name' => 'core/block', + 'attributes' => [ 'ref' => -1, ], // inner_blocks is omitted when empty for backwards compatibility with earlier release From fc2457531a99621d2d8ca62c1a2728df8f0e7fbf Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 13:59:31 -0400 Subject: [PATCH 7/7] use WP_Post --- src/parser/block-additions/core-block.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/block-additions/core-block.php b/src/parser/block-additions/core-block.php index 2e259cf..c76fd74 100644 --- a/src/parser/block-additions/core-block.php +++ b/src/parser/block-additions/core-block.php @@ -10,6 +10,7 @@ defined( 'ABSPATH' ) || die(); use WP_Block; +use WP_Post; use WPCOMVIP\BlockDataApi\ContentParser; use function add_filter; @@ -84,7 +85,7 @@ public static function get_inner_blocks( array $inner_blocks, string $block_name $parser = new ContentParser(); $post = get_post( $parsed_block['attrs']['ref'] ); - if ( ! $post instanceof \WP_Post ) { + if ( ! $post instanceof WP_Post ) { return []; }