diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index 192f47fe..4cdec649 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -40,7 +40,7 @@
Tests for WordPress version compatibility.
https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties
-->
-
+
diff --git a/includes/Data/ContentBlocksResolver.php b/includes/Data/ContentBlocksResolver.php
index 3655362d..a7493b3e 100644
--- a/includes/Data/ContentBlocksResolver.php
+++ b/includes/Data/ContentBlocksResolver.php
@@ -7,7 +7,6 @@
namespace WPGraphQL\ContentBlocks\Data;
-use WPGraphQL\ContentBlocks\Utilities\TraverseHelpers;
use WPGraphQL\Model\Post;
/**
@@ -17,9 +16,9 @@ final class ContentBlocksResolver {
/**
* Retrieves a list of content blocks
*
- * @param mixed $node The node we are resolving.
- * @param array $args GraphQL query args to pass to the connection resolver.
- * @param array $allowed_block_names The list of allowed block names to filter.
+ * @param \WPGraphQL\Model\Model|mixed $node The node we are resolving.
+ * @param array $args GraphQL query args to pass to the connection resolver.
+ * @param string[] $allowed_block_names The list of allowed block names to filter.
*/
public static function resolve_content_blocks( $node, $args, $allowed_block_names = [] ): array {
/**
@@ -64,57 +63,18 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name
}
// Parse the blocks from HTML comments to an array of blocks
- $parsed_blocks = parse_blocks( $content );
+ $parsed_blocks = self::parse_blocks( $content );
if ( empty( $parsed_blocks ) ) {
return [];
}
- // 1st Level filtering of blocks that are empty
- $parsed_blocks = array_filter(
- $parsed_blocks,
- static function ( $parsed_block ) {
- if ( ! empty( $parsed_block['blockName'] ) ) {
- return true;
- }
-
- // Strip empty comments and spaces
- $stripped = preg_replace( '//Uis', '', render_block( $parsed_block ) );
- return ! empty( trim( $stripped ?? '' ) );
- },
- ARRAY_FILTER_USE_BOTH
- );
-
- // 2nd Level assigning of unique id's and missing blockNames
- $parsed_blocks = array_map(
- static function ( $parsed_block ) {
- $parsed_block['clientId'] = uniqid();
- // Since Gutenberg assigns an empty blockName for Classic block
- // we define the name here
- if ( empty( $parsed_block['blockName'] ) ) {
- $parsed_block['blockName'] = 'core/freeform';
- }
- return $parsed_block;
- },
- $parsed_blocks
- );
-
- // Resolve reusable blocks - replaces "core/block" with the corresponding block(s) from the reusable ref ID
- TraverseHelpers::traverse_blocks( $parsed_blocks, [ TraverseHelpers::class, 'replace_reusable_blocks' ], 0, PHP_INT_MAX );
// Flatten block list here if requested or if 'flat' value is not selected (default)
if ( ! isset( $args['flat'] ) || 'true' == $args['flat'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
$parsed_blocks = self::flatten_block_list( $parsed_blocks );
}
// Final level of filtering out blocks not in the allowed list
- if ( ! empty( $allowed_block_names ) ) {
- $parsed_blocks = array_filter(
- $parsed_blocks,
- static function ( $parsed_block ) use ( $allowed_block_names ) {
- return in_array( $parsed_block['blockName'], $allowed_block_names, true );
- },
- ARRAY_FILTER_USE_BOTH
- );
- }
+ $parsed_blocks = self::filter_allowed_blocks( $parsed_blocks, $allowed_block_names );
/**
* Filters the content blocks after they have been resolved.
@@ -129,6 +89,120 @@ static function ( $parsed_block ) use ( $allowed_block_names ) {
return is_array( $parsed_blocks ) ? $parsed_blocks : [];
}
+ /**
+ * Get blocks from html string.
+ *
+ * @param string $content Content to parse.
+ *
+ * @return array List of blocks.
+ */
+ private static function parse_blocks( $content ): array {
+ $blocks = parse_blocks( $content );
+
+ return self::handle_do_blocks( $blocks );
+ }
+
+ /**
+ * Recursively process blocks.
+ *
+ * This mirrors the `do_blocks` function in WordPress which is responsible for hydrating certain block attributes and supports, but without the forced rendering.
+ *
+ * @param array[] $blocks Blocks data.
+ *
+ * @return array[] The processed blocks.
+ */
+ private static function handle_do_blocks( array $blocks ): array {
+ $parsed = [];
+ foreach ( $blocks as $block ) {
+ $block_data = self::handle_do_block( $block );
+
+ if ( $block_data ) {
+ $parsed[] = $block_data;
+ }
+ }
+
+ // Remove empty blocks.
+ return array_filter( $parsed );
+ }
+
+ /**
+ * Process a block, getting all extra fields.
+ *
+ * @param array $block Block data.
+ *
+ * @return ?array The processed block.
+ */
+ private static function handle_do_block( array $block ): ?array {
+ if ( self::is_block_empty( $block ) ) {
+ return null;
+ }
+
+ // Since Gutenberg assigns an empty blockName for Classic block, we define it here.
+ if ( empty( $block['blockName'] ) ) {
+ $block['blockName'] = 'core/freeform';
+ }
+
+ // Assign a unique clientId to the block.
+ $block['clientId'] = uniqid();
+
+ // @todo apply more hydrations.
+
+ $block = self::populate_reusable_blocks( $block );
+
+ // Prepare innerBlocks.
+ if ( ! empty( $block['innerBlocks'] ) ) {
+ $block['innerBlocks'] = self::handle_do_blocks( $block['innerBlocks'] );
+ }
+
+ return $block;
+ }
+
+ /**
+ * Checks whether a block is really empty, and not just a `core/freeform`.
+ *
+ * @param array $block The block to check.
+ */
+ private static function is_block_empty( array $block ): bool {
+ // If we have a blockName, no need to check further.
+ if ( ! empty( $block['blockName'] ) ) {
+ return false;
+ }
+
+ // @todo add more checks and avoid using render_block().
+
+ // Strip empty comments and spaces
+ $stripped = preg_replace( '//Uis', '', render_block( $block ) );
+
+ return empty( trim( $stripped ?? '' ) );
+ }
+
+ /**
+ * Populates reusable blocks with the blocks from the reusable ref ID.
+ *
+ * @param array $block The block to populate.
+ *
+ * @return array The populated block.
+ */
+ private static function populate_reusable_blocks( array $block ): array {
+ if ( 'core/block' !== $block['blockName'] || ! isset( $block['attrs']['ref'] ) ) {
+ return $block;
+ }
+
+ $reusable_block = get_post( $block['attrs']['ref'] );
+
+ if ( ! $reusable_block ) {
+ return $block;
+ }
+
+ $parsed_blocks = ! empty( $reusable_block->post_content ) ? self::parse_blocks( $reusable_block->post_content ) : null;
+
+ if ( empty( $parsed_blocks ) ) {
+ return $block;
+ }
+
+ return array_merge( ...$parsed_blocks );
+ }
+
/**
* Flattens a list blocks into a single array
*
@@ -145,16 +219,41 @@ private static function flatten_block_list( $blocks ): array {
/**
* Flattens a block and its inner blocks into a single while attaching unique clientId's
*
- * @param mixed $block A block.
+ * @param array $block A parsed block.
*/
private static function flatten_inner_blocks( $block ): array {
- $result = [];
+ $result = [];
+
+ // Assign a unique clientId to the block if it doesn't already have one.
$block['clientId'] = isset( $block['clientId'] ) ? $block['clientId'] : uniqid();
array_push( $result, $block );
+
foreach ( $block['innerBlocks'] as $child ) {
$child['parentClientId'] = $block['clientId'];
- $result = array_merge( $result, self::flatten_inner_blocks( $child ) );
+
+ // Flatten the child, and merge with the result.
+ $result = array_merge( $result, self::flatten_inner_blocks( $child ) );
}
+
return $result;
}
+
+ /**
+ * Filters out disallowed blocks from the list of blocks
+ *
+ * @param array $blocks A list of blocks to filter.
+ * @param string[] $allowed_block_names The list of allowed block names to filter.
+ */
+ private static function filter_allowed_blocks( array $blocks, array $allowed_block_names ): array {
+ if ( empty( $allowed_block_names ) ) {
+ return $blocks;
+ }
+
+ return array_filter(
+ $blocks,
+ static function ( $block ) use ( $allowed_block_names ) {
+ return in_array( $block['blockName'], $allowed_block_names, true );
+ }
+ );
+ }
}
diff --git a/includes/Utilities/TraverseHelpers.php b/includes/Utilities/TraverseHelpers.php
index 2a4064cc..bc2ed91d 100644
--- a/includes/Utilities/TraverseHelpers.php
+++ b/includes/Utilities/TraverseHelpers.php
@@ -7,21 +7,37 @@
namespace WPGraphQL\ContentBlocks\Utilities;
+use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver;
+
/**
* Class TraverseHelpers
*
* Provides utility functions to traverse and manipulate blocks.
+ *
+ * @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
*/
final class TraverseHelpers {
/**
* Traverse blocks and apply a callback with optional depth limit.
*
+ * @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
+ *
* @param array &$blocks The blocks to traverse.
* @param callable $callback The callback function to apply to each block.
* @param int $depth The current depth of traversal.
* @param int $max_depth The maximum depth to traverse.
*/
public static function traverse_blocks( &$blocks, $callback, $depth = 0, $max_depth = PHP_INT_MAX ): void {
+ _deprecated_function(
+ __METHOD__,
+ '@todo',
+ sprintf(
+ // translators: %s: class name
+ esc_html__( 'Manipulate blocks directly inside %s::handle_do_block', 'wp-graphql-content-blocks' ),
+ ContentBlocksResolver::class
+ )
+ );
+
foreach ( $blocks as &$block ) {
$callback( $block );
if ( ! empty( $block['innerBlocks'] ) && $depth < $max_depth ) {
@@ -33,9 +49,13 @@ public static function traverse_blocks( &$blocks, $callback, $depth = 0, $max_de
/**
* Example callback function to replace reusable blocks.
*
+ * @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
+ *
* @param array $block The block to potentially replace.
*/
public static function replace_reusable_blocks( &$block ): void {
+ _deprecated_function( __METHOD__, '@todo', ContentBlocksResolver::class . '::populate_reusable_blocks' );
+
if ( 'core/block' === $block['blockName'] && isset( $block['attrs']['ref'] ) ) {
$post = get_post( $block['attrs']['ref'] );
$reusable_blocks = ! empty( $post->post_content ) ? parse_blocks( $post->post_content ) : null;
diff --git a/tests/unit/TraverseHelpersTest.php b/tests/unit/TraverseHelpersTest.php
deleted file mode 100644
index ec710462..00000000
--- a/tests/unit/TraverseHelpersTest.php
+++ /dev/null
@@ -1,85 +0,0 @@
-post_id = wp_insert_post(
- array(
- 'post_title' => 'Post Title',
- 'post_content' => preg_replace(
- '/\s+/',
- ' ',
- trim(
- '
-
- Test
- '
- )
- ),
- 'post_status' => 'publish',
- )
- );
- }
-
- public function tearDown(): void {
- // your tear down methods here
- parent::tearDown();
- wp_delete_post( $this->post_id, true );
- }
- public function testTraverseBlocks() {
- // Sample blocks data
- $blocks = [
- [
- 'blockName' => 'core/group',
- 'attrs' => [],
- 'innerBlocks' => [
- [
- 'blockName' => 'core/block',
- 'attrs' => [ 'ref' => $this->post_id ],
- 'innerBlocks' => []
- ]
- ]
- ],
- [
- 'blockName' => 'core/block',
- 'attrs' => [ 'ref' => $this->post_id ],
- 'innerBlocks' => []
- ]
- ];
-
- // Expected result after replacing reusable blocks
- $expected = [
- [
- 'blockName' => 'core/group',
- 'attrs' => [],
- 'innerBlocks' => [
- [
- 'blockName' => 'core/paragraph',
- 'attrs' => [],
- 'innerBlocks' => [],
- 'innerHTML' => ' Test
',
- 'innerContent' => [ 0 => ' Test
']
- ]
- ]
- ],
- [
- 'blockName' => 'core/paragraph',
- 'attrs' => [],
- 'innerBlocks' => [],
- 'innerHTML' => ' Test
',
- 'innerContent' => [ 0 => ' Test
']
- ]
- ];
-
- TraverseHelpers::traverse_blocks( $blocks, [ TraverseHelpers::class, 'replace_reusable_blocks' ], 0, PHP_INT_MAX );
- $this->assertEquals( $expected, $blocks );
- }
-}
\ No newline at end of file