From aa033cba5cd3424a0445c6cd95e34307986880a1 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 21 Sep 2023 08:32:52 +0000 Subject: [PATCH] Blocks: Change `traverse_and_serialize_block(s)`'s callback signature. During work on #59399, it was discovered that ''sibling'' block insertion wasn't likely going to work the way it was planned, which required devising an alternative solution. This new solution requires some changes to `traverse_and_serialize_block(s)`: - Change the signature of the existing callback such that: - the return value is a string that will be prepended to the result of the inner block traversal and serialization; - the function arguments are: a ''reference'' to the current block (so it can be modified inline, which is important e.g. for `theme` attribute insertion), the parent block, and the previous block (instead of the block index and chunk index). - Add a second callback argument to `traverse_and_serialize_block(s)`, which is called ''after'' the block is traversed and serialized. - Its function arguments are a reference to the current block, the parent block, and the next block. Props gziolo. Fixes #59412. See #59313. git-svn-id: https://develop.svn.wordpress.org/trunk@56644 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-template-utils.php | 5 +- src/wp-includes/blocks.php | 124 ++++++++++++++----- tests/phpunit/tests/block-template-utils.php | 20 +-- tests/phpunit/tests/blocks/serialize.php | 12 +- 4 files changed, 110 insertions(+), 51 deletions(-) diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 808e83e7b9..9e0eafcc99 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -515,16 +515,15 @@ function _inject_theme_attribute_in_block_template_content( $template_content ) * @access private * * @param array $block a parsed block. - * @return array Updated block. + * @return void */ -function _inject_theme_attribute_in_template_part_block( $block ) { +function _inject_theme_attribute_in_template_part_block( &$block ) { if ( 'core/template-part' === $block['blockName'] && ! isset( $block['attrs']['theme'] ) ) { $block['attrs']['theme'] = get_stylesheet(); } - return $block; } /** diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index fe9c56a551..b7f36cff38 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -886,38 +886,68 @@ function serialize_blocks( $blocks ) { } /** - * Traverses the block applying transformations using the callback provided and returns the content of a block, - * including comment delimiters, serializing all attributes from the given parsed block. + * Traverses a parsed block tree and applies callbacks before and after serializing it. * - * This should be used when there is a need to modify the saved block. - * Prefer `serialize_block` when preparing a block to be saved to post content. + * Recursively traverses the block and its inner blocks and applies the two callbacks provided as + * arguments, the first one before serializing the block, and the second one after serializing it. + * If either callback returns a string value, it will be prepended and appended to the serialized + * block markup, respectively. + * + * The callbacks will receive a reference to the current block as their first argument, so that they + * can also modify it, and the current block's parent block as second argument. Finally, the + * `$pre_callback` receives the previous block, whereas the `$post_callback` receives + * the next block as third argument. + * + * Serialized blocks are returned including comment delimiters, and with all attributes serialized. + * + * This function should be used when there is a need to modify the saved block, or to inject markup + * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. * * @since 6.4.0 * * @see serialize_block() * - * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. - * @param callable $callback Callback to run on each block in the tree before serialization. - * It is called with the following arguments: $block, $parent_block, $block_index, $chunk_index. - * @return string String of rendered HTML. + * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. + * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. + * It is called with the following arguments: &$block, $parent_block, $previous_block. + * Its string return value will be prepended to the serialized block markup. + * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. + * It is called with the following arguments: &$block, $parent_block, $next_block. + * Its string return value will be appended to the serialized block markup. + * @return string Serialized block markup. */ -function traverse_and_serialize_block( $block, $callback ) { +function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { $block_content = ''; $block_index = 0; - foreach ( $block['innerContent'] as $chunk_index => $chunk ) { + foreach ( $block['innerContent'] as $chunk ) { if ( is_string( $chunk ) ) { $block_content .= $chunk; } else { - $inner_block = call_user_func( - $callback, - $block['innerBlocks'][ $block_index ], - $block, - $block_index, - $chunk_index - ); + $inner_block = $block['innerBlocks'][ $block_index ]; + + if ( is_callable( $pre_callback ) ) { + $prev = 0 === $block_index + ? null + : $block['innerBlocks'][ $block_index - 1 ]; + $block_content .= call_user_func_array( + $pre_callback, + array( &$inner_block, $block, $prev ) + ); + } + + $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); + + if ( is_callable( $post_callback ) ) { + $next = count( $block['innerBlocks'] ) - 1 === $block_index + ? null + : $block['innerBlocks'][ $block_index + 1 ]; + $block_content .= call_user_func_array( + $post_callback, + array( &$inner_block, $block, $next ) + ); + } $block_index++; - $block_content .= traverse_and_serialize_block( $inner_block, $callback ); } } @@ -933,27 +963,59 @@ function traverse_and_serialize_block( $block, $callback ) { } /** - * Traverses the blocks applying transformations using the callback provided, - * and returns a joined string of the aggregate serialization of the given parsed blocks. + * Given an array of parsed block trees, applies callbacks before and after serializing them and + * returns their concatenated output. * - * This should be used when there is a need to modify the saved blocks. - * Prefer `serialize_blocks` when preparing blocks to be saved to post content. + * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as + * arguments, the first one before serializing a block, and the second one after serializing. + * If either callback returns a string value, it will be prepended and appended to the serialized + * block markup, respectively. + * + * The callbacks will receive a reference to the current block as their first argument, so that they + * can also modify it, and the current block's parent block as second argument. Finally, the + * `$pre_callback` receives the previous block, whereas the `$post_callback` receives + * the next block as third argument. + * + * Serialized blocks are returned including comment delimiters, and with all attributes serialized. + * + * This function should be used when there is a need to modify the saved blocks, or to inject markup + * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. * * @since 6.4.0 * * @see serialize_blocks() * - * @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). - * @param callable $callback Callback to run on each block in the tree before serialization. - * It is called with the following arguments: $block, $parent_block, $block_index, $chunk_index. - * @return string String of rendered HTML. + * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. + * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. + * It is called with the following arguments: &$block, $parent_block, $previous_block. + * Its string return value will be prepended to the serialized block markup. + * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. + * It is called with the following arguments: &$block, $parent_block, $next_block. + * Its string return value will be appended to the serialized block markup. + * @return string Serialized block markup. */ -function traverse_and_serialize_blocks( $blocks, $callback ) { +function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { $result = ''; - foreach ( $blocks as $block ) { - // At the top level, there is no parent block, block index, or chunk index to pass to the callback. - $block = call_user_func( $callback, $block ); - $result .= traverse_and_serialize_block( $block, $callback ); + foreach ( $blocks as $index => $block ) { + if ( is_callable( $pre_callback ) ) { + $prev = 0 === $index + ? null + : $blocks[ $index - 1 ]; + $result .= call_user_func_array( + $pre_callback, + array( &$block, null, $prev ) // At the top level, there is no parent block to pass to the callback. + ); + } + $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); + if ( is_callable( $post_callback ) ) { + $next = count( $blocks ) - 1 === $index + ? null + : $blocks[ $index + 1 ]; + $result .= call_user_func_array( + $post_callback, + array( &$block, null, $next ) // At the top level, there is no parent block to pass to the callback. + ); + } } return $result; } diff --git a/tests/phpunit/tests/block-template-utils.php b/tests/phpunit/tests/block-template-utils.php index bb8a25cba6..e7ee300062 100644 --- a/tests/phpunit/tests/block-template-utils.php +++ b/tests/phpunit/tests/block-template-utils.php @@ -225,7 +225,7 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { * @covers ::_inject_theme_attribute_in_template_part_block */ public function test_inject_theme_attribute_in_template_part_block() { - $template_part_block_without_theme_attribute = array( + $template_part_block = array( 'blockName' => 'core/template-part', 'attrs' => array( 'slug' => 'header', @@ -238,7 +238,7 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { 'innerBlocks' => array(), ); - $actual = _inject_theme_attribute_in_template_part_block( $template_part_block_without_theme_attribute ); + _inject_theme_attribute_in_template_part_block( $template_part_block ); $expected = array( 'blockName' => 'core/template-part', 'attrs' => array( @@ -254,7 +254,7 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { ); $this->assertSame( $expected, - $actual, + $template_part_block, '`theme` attribute was not correctly injected in template part block.' ); } @@ -265,7 +265,7 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { * @covers ::_inject_theme_attribute_in_template_part_block */ public function test_not_inject_theme_attribute_in_template_part_block_theme_attribute_exists() { - $template_part_block_with_existing_theme_attribute = array( + $template_part_block = array( 'blockName' => 'core/template-part', 'attrs' => array( 'slug' => 'header', @@ -279,10 +279,11 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { 'innerBlocks' => array(), ); - $actual = _inject_theme_attribute_in_template_part_block( $template_part_block_with_existing_theme_attribute ); + $expected = $template_part_block; + _inject_theme_attribute_in_template_part_block( $template_part_block ); $this->assertSame( - $template_part_block_with_existing_theme_attribute, - $actual, + $expected, + $template_part_block, 'Existing `theme` attribute in template part block was not respected by attribute injection.' ); } @@ -301,10 +302,11 @@ class Tests_Block_Template_Utils extends WP_UnitTestCase { 'innerBlocks' => array(), ); - $actual = _inject_theme_attribute_in_template_part_block( $non_template_part_block ); + $expected = $non_template_part_block; + _inject_theme_attribute_in_template_part_block( $non_template_part_block ); $this->assertSame( + $expected, $non_template_part_block, - $actual, '`theme` attribute injection modified non-template-part block.' ); } diff --git a/tests/phpunit/tests/blocks/serialize.php b/tests/phpunit/tests/blocks/serialize.php index 533e8f5c63..484f4c6974 100644 --- a/tests/phpunit/tests/blocks/serialize.php +++ b/tests/phpunit/tests/blocks/serialize.php @@ -58,6 +58,7 @@ class Tests_Blocks_Serialize extends WP_UnitTestCase { /** * @ticket 59327 + * @ticket 59412 * * @covers ::traverse_and_serialize_blocks */ @@ -73,15 +74,15 @@ class Tests_Blocks_Serialize extends WP_UnitTestCase { ); } - public static function add_attribute_to_inner_block( $block ) { + public static function add_attribute_to_inner_block( &$block ) { if ( 'core/inner' === $block['blockName'] ) { $block['attrs']['myattr'] = 'myvalue'; } - return $block; } /** * @ticket 59327 + * @ticket 59412 * * @covers ::traverse_and_serialize_blocks * @@ -92,12 +93,7 @@ class Tests_Blocks_Serialize extends WP_UnitTestCase { public function test_traverse_and_serialize_identity_from_parsed( $original ) { $blocks = parse_blocks( $original ); - $actual = traverse_and_serialize_blocks( - $blocks, - function ( $block ) { - return $block; - } - ); + $actual = traverse_and_serialize_blocks( $blocks ); $this->assertSame( $original, $actual ); }