From 1c554d6a2f1abf1c1484e26b7054082e669fa061 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 18 Oct 2024 10:42:41 +0000 Subject: [PATCH 1/7] feat!: implement model (WIP) --- includes/Blocks/Block.php | 23 +-- includes/Blocks/CoreImage.php | 3 +- includes/Data/ContentBlocksResolver.php | 12 +- includes/Field/BlockSupports/Anchor.php | 4 +- includes/Model/Block.php | 167 ++++++++++++++++++ .../InterfaceType/EditorBlockInterface.php | 30 +--- .../InterfaceType/PostTypeBlockInterface.php | 3 +- includes/Utilities/DOMHelpers.php | 8 +- tests/unit/ContentBlocksResolverTest.php | 98 +++++----- 9 files changed, 248 insertions(+), 100 deletions(-) create mode 100644 includes/Model/Block.php diff --git a/includes/Blocks/Block.php b/includes/Blocks/Block.php index 94258282..8a4272b2 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -8,6 +8,7 @@ namespace WPGraphQL\ContentBlocks\Blocks; use WPGraphQL\ContentBlocks\Data\BlockAttributeResolver; +use \WPGraphQL\ContentBlocks\Model\Block as BlockModel; use WPGraphQL\ContentBlocks\Registry\Registry; use WPGraphQL\ContentBlocks\Type\Scalar\Scalar; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; @@ -108,6 +109,7 @@ private function register_block_attributes_as_fields(): void { 'fields' => $block_attribute_fields, ] ); + register_graphql_field( $this->type_name, 'attributes', @@ -219,8 +221,9 @@ private function get_block_attribute_fields( ?array $block_attributes, string $p $config = [ $attribute_name => $attribute_config, ]; - $result = $this->resolve_block_attributes_recursive( $block['attrs'], wp_unslash( render_block( $block ) ), $config ); + $result = $this->resolve_block_attributes_recursive( $block, $config ); + // Normalize the value. return $result[ $attribute_name ]; }, ]; @@ -325,7 +328,9 @@ private function normalize_attribute_value( $value, $type ) { } /** - * Register the Type for the block. This happens after all other object types are already registered. + * Register the Type for the block. + * + * This happens after all other object types are already registered. */ private function register_type(): void { register_graphql_object_type( @@ -338,9 +343,6 @@ private function register_type(): void { 'name' => [ 'type' => 'String', 'description' => __( 'The name of the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['blockName'] ) ? (string) $block['blockName'] : null; - }, ], ], ] @@ -368,18 +370,17 @@ private function get_block_attributes_interfaces(): array { /** * Resolved the value of the block attributes based on the specified config * - * @param array $attribute_values The block current attributes value. - * @param string $html The block rendered html. - * @param array $attribute_configs The block current attribute configuration, keyed to the attribute name. + * @param \WPGraphQL\ContentBlocks\Model\Block $block The block model instance. + * @param array $attribute_configs The block current attribute configuration, keyed to the attribute name. */ - private function resolve_block_attributes_recursive( $attribute_values, string $html, array $attribute_configs ): array { + private function resolve_block_attributes_recursive( BlockModel $block, array $attribute_configs ): array { $result = []; // Clean up the html. - $html = trim( $html ); + $html = isset( $block->renderedHtml ) ? trim( $block->renderedHtml ) : ''; foreach ( $attribute_configs as $key => $config ) { - $attribute_value = $attribute_values[ $key ] ?? null; + $attribute_value = $block->parsedAttributes[ $key ] ?? null; $result[ $key ] = BlockAttributeResolver::resolve_block_attribute( $config, $html, $attribute_value ); } diff --git a/includes/Blocks/CoreImage.php b/includes/Blocks/CoreImage.php index caf6c81a..532299c6 100644 --- a/includes/Blocks/CoreImage.php +++ b/includes/Blocks/CoreImage.php @@ -55,8 +55,9 @@ public function __construct( WP_Block_Type $block, Registry $block_registry ) { $this->type_name ), 'resolve' => static function ( $block ) { - $attrs = $block['attrs']; + $attrs = $block->parsedAttributes ?? []; $id = $attrs['id'] ?? null; + if ( $id ) { $media_details = wp_get_attachment_metadata( $id ); if ( ! empty( $media_details ) ) { diff --git a/includes/Data/ContentBlocksResolver.php b/includes/Data/ContentBlocksResolver.php index dcf6c43c..be717e48 100644 --- a/includes/Data/ContentBlocksResolver.php +++ b/includes/Data/ContentBlocksResolver.php @@ -7,6 +7,7 @@ namespace WPGraphQL\ContentBlocks\Data; +use WPGraphQL\ContentBlocks\Model\Block; use WPGraphQL\Model\Post; /** @@ -20,7 +21,7 @@ final class ContentBlocksResolver { * @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. * - * @return array The resolved parsed blocks. + * @return \WPGraphQL\ContentBlocks\Model\Block[] */ public static function resolve_content_blocks( $node, $args, $allowed_block_names = [] ): array { /** @@ -39,7 +40,6 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name $content = null; if ( $node instanceof Post ) { - // @todo: this is restricted intentionally. // $content = $node->contentRaw; @@ -88,7 +88,13 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name */ $parsed_blocks = apply_filters( 'wpgraphql_content_blocks_resolve_blocks', $parsed_blocks, $node, $args, $allowed_block_names ); - return is_array( $parsed_blocks ) ? $parsed_blocks : []; + // Map the blocks to the Block model + return is_array( $parsed_blocks ) ? array_map( + static function ( $parsed_block ) { + return new Block( new \WP_Block( $parsed_block ) ); + }, + $parsed_blocks + ) : []; } /** diff --git a/includes/Field/BlockSupports/Anchor.php b/includes/Field/BlockSupports/Anchor.php index 4ca1ba02..49ad3409 100644 --- a/includes/Field/BlockSupports/Anchor.php +++ b/includes/Field/BlockSupports/Anchor.php @@ -27,10 +27,12 @@ public static function register(): void { 'type' => 'string', 'description' => __( 'The anchor field for the block.', 'wp-graphql-content-blocks' ), 'resolve' => static function ( $block ) { - $rendered_block = wp_unslash( render_block( $block ) ); + $rendered_block = wp_unslash( $block->renderedHtml ); + if ( empty( $rendered_block ) ) { return null; } + return DOMHelpers::parse_first_node_attribute( $rendered_block, 'id' ); }, ], diff --git a/includes/Model/Block.php b/includes/Model/Block.php new file mode 100644 index 00000000..b2bf64c8 --- /dev/null +++ b/includes/Model/Block.php @@ -0,0 +1,167 @@ + $parsedAttributes + * @property ?string $type + * @property \WP_Block $wpBlock + */ +class Block extends Model { + /** + * The underlying \WP_Block instance for the block data. + * + * @var \WP_Block + */ + protected $data; + + /** + * The rendered block html. + * + * @var ?string + */ + protected $rendered_block; + + /** + * {@inheritDoc} + * + * @param \WP_Block $block The block data to be modeled. + */ + public function __construct( \WP_Block $block ) { + $this->data = $block; + + // Log a Debug message if the block type is not found. + if ( ! $this->data->block_type ) { + graphql_debug( + sprintf( + __( 'Block type not found for block: %s', 'wp-graphql-content-blocks' ), + $this->data->name ?: 'Unknown' + ), + [ + 'parsed_block' => $block, + ] + ); + } + + parent::__construct(); + } + + /** + * {@inheritDoc} + */ + protected function is_private() { + return false; + } + + /** + * {@inheritDoc} + */ + protected function init() { + if ( empty( $this->fields ) ) { + $this->fields = [ + 'clientId' => fn (): ?string => $this->data->parsed_block['clientId'] ?? uniqid(), + 'parentClientId' => fn (): ?string => $this->data->parsed_block['parentClientId'] ?? null, + 'name' => fn (): ?string => $this->data->name ?: null, + 'blockEditorCategoryName' => fn () => isset( $this->data->block_type->category ) ? $this->data->block_type->category : null, + 'isDynamic' => fn (): bool => is_callable( $this->data->block_type->render_callback ), + 'apiVersion' => fn (): ?int => $this->data->block_type->api_version ?: null, + 'cssClassNames' => fn (): ?array => isset( $this->data->attributes['className'] ) ? explode( ' ', $this->data->attributes['className'] ) : null, + 'renderedHtml' => fn (): ?string => $this->get_rendered_block(), + 'innerBlocks' => function (): array { + $block_list = $this->data->inner_blocks ?: []; + + $models_to_return = []; + + foreach ( $block_list as $block ) { + $models_to_return[] = new self( $block ); + } + + return $models_to_return; + }, + 'parsedAttributes' => fn (): array => $this->data->attributes, + 'type' => function (): ?string { + $block_name = $this->name ?? null; + + return isset( $block_name ) ? WPGraphQLHelpers::format_type_name( $block_name ) : null; + }, + 'wpBlock' => function (): \WP_Block { + return $this->data; + }, + ]; + } + } + + /** + * Renders the block html - only once. + * + * The `render_block()` function causes side effects (such as globally-incrementing the counter used for layout styles), so we only want to call it once. + */ + protected function get_rendered_block(): ?string { + if ( ! isset( $this->rendered_block ) ) { + $rendered = $this->data->render(); + $this->rendered_block = do_shortcode( $rendered ); + } + + return $this->rendered_block; + } + + /** + * Resolves the block attributes. + * + * @return array):mixed> + */ + protected function resolve_attributes(): array { + $registered_attributes = $this->data->block_type->attributes ?? []; + + $resolvers = []; + + foreach ( array_keys( $registered_attributes ) as $attribute_name ) { + $resolvers[ $attribute_name ] = fn () => $this->resolve_attribute( $attribute_name ); + } + + return $resolvers; + } + + /** + * Resolves a single block attribute. + * + * @param string $attribute_name The name of the attribute being resolved. + * + * @return mixed + */ + protected function resolve_attribute( string $attribute_name ) { + $attribute_config = $this->data->block_type->attributes[ $attribute_name ] ?? []; + + return BlockAttributeResolver::resolve_block_attribute( $attribute_config, $this->get_rendered_block(), $this->data->attributes[ $attribute_name ] ); + + $allowed_types = [ 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ]; + // If attribute type is set and valid, sanitize value. + if ( isset( $attribute['type'] ) && in_array( $attribute_config['type'], $allowed_types, true ) && rest_validate_value_from_schema( $result, $attribute_config ) ) { + $result = rest_sanitize_value_from_schema( $result, $attribute_config ); + } + + return $result; + } +} diff --git a/includes/Type/InterfaceType/EditorBlockInterface.php b/includes/Type/InterfaceType/EditorBlockInterface.php index 5aa8e760..cab03027 100644 --- a/includes/Type/InterfaceType/EditorBlockInterface.php +++ b/includes/Type/InterfaceType/EditorBlockInterface.php @@ -8,7 +8,6 @@ namespace WPGraphQL\ContentBlocks\Type\InterfaceType; use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; -use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; use WP_Block_Type_Registry; /** @@ -75,16 +74,10 @@ public static function register_type(): void { 'clientId' => [ 'type' => 'String', 'description' => __( 'The id of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['clientId'] ) ? $block['clientId'] : uniqid(); - }, ], 'parentClientId' => [ 'type' => 'String', 'description' => __( 'The parent id of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['parentClientId'] ) ? $block['parentClientId'] : null; - }, ], 'name' => [ 'type' => 'String', @@ -93,22 +86,16 @@ public static function register_type(): void { 'blockEditorCategoryName' => [ 'type' => 'String', 'description' => __( 'The name of the category the Block belongs to', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->category ) ? self::get_block( $block )->category : null; - }, ], 'isDynamic' => [ 'type' => [ 'non_null' => 'Boolean' ], 'description' => __( 'Whether the block is Dynamic (server rendered)', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->render_callback ); - }, ], 'apiVersion' => [ 'type' => 'Integer', 'description' => __( 'The API version of the Gutenberg Block', 'wp-graphql-content-blocks' ), 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->api_version ) && absint( self::get_block( $block )->api_version ) ? absint( self::get_block( $block )->api_version ) : 2; + return $block->apiVersion ?? 2; }, ], 'innerBlocks' => [ @@ -116,27 +103,14 @@ public static function register_type(): void { 'list_of' => 'EditorBlock', ], 'description' => __( 'The inner blocks of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : []; - }, ], 'cssClassNames' => [ 'type' => [ 'list_of' => 'String' ], 'description' => __( 'CSS Classnames to apply to the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - if ( isset( $block['attrs']['className'] ) ) { - return explode( ' ', $block['attrs']['className'] ); - } - - return null; - }, ], 'renderedHtml' => [ 'type' => 'String', 'description' => __( 'The rendered HTML for the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return render_block( $block ); - }, ], 'type' => [ 'type' => 'String', @@ -147,7 +121,7 @@ public static function register_type(): void { ], ], 'resolveType' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + return $block->type; }, ] ); diff --git a/includes/Type/InterfaceType/PostTypeBlockInterface.php b/includes/Type/InterfaceType/PostTypeBlockInterface.php index f65e4d88..e77e4cbf 100644 --- a/includes/Type/InterfaceType/PostTypeBlockInterface.php +++ b/includes/Type/InterfaceType/PostTypeBlockInterface.php @@ -8,7 +8,6 @@ namespace WPGraphQL\ContentBlocks\Type\InterfaceType; use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; -use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; /** * Class PostTypeBlockInterface @@ -36,7 +35,7 @@ public static function register_type( string $post_type, array $block_names = [] ], ], 'resolveType' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + return $block->type; }, ] ); diff --git a/includes/Utilities/DOMHelpers.php b/includes/Utilities/DOMHelpers.php index 7f5cec79..4f76b879 100644 --- a/includes/Utilities/DOMHelpers.php +++ b/includes/Utilities/DOMHelpers.php @@ -19,11 +19,10 @@ final class DOMHelpers { * @param string $html The HTML string to parse. * @param string $selector The selector to use. * @param string $attribute The attribute to extract. - * @param mixed $default_value The default value to return if the selector is not found. * * @return ?string extracted attribute */ - public static function parse_attribute( string $html, string $selector, string $attribute, $default_value = null ): ?string { + public static function parse_attribute( string $html, string $selector, string $attribute ): ?string { // Bail early if there's no html to parse. if ( empty( trim( $html ) ) ) { return null; @@ -39,7 +38,6 @@ public static function parse_attribute( string $html, string $selector, string $ } $nodes = $doc->find( $selector ); - $default_value = isset( $default_value ) ? $default_value : null; foreach ( $nodes as $node ) { if ( $node->hasAttribute( $attribute ) ) { @@ -47,7 +45,7 @@ public static function parse_attribute( string $html, string $selector, string $ } } - return $default_value; + return null; } /** @@ -240,7 +238,7 @@ public static function find_nodes( string $html, ?string $selector = null ) { public static function parseAttribute( $html, $selector, $attribute, $default_value = null ): ?string { _deprecated_function( __METHOD__, '4.2.0', self::class . '::parse_attribute' ); - return self::parse_attribute( $html, $selector, $attribute, $default_value ); + return self::parse_attribute( $html, $selector, $attribute ) ?: $default_value; } /** diff --git a/tests/unit/ContentBlocksResolverTest.php b/tests/unit/ContentBlocksResolverTest.php index dea2faae..344e67cd 100644 --- a/tests/unit/ContentBlocksResolverTest.php +++ b/tests/unit/ContentBlocksResolverTest.php @@ -103,7 +103,7 @@ public function test_resolve_content_blocks_resolves_reusable_blocks() { // There should return only the non-empty blocks $this->assertEquals( 3, count( $actual ) ); - $this->assertEquals( 'core/columns', $actual[0]['blockName'] ); + $this->assertEquals( 'core/columns', $actual[0]->name ); } public function test_resolve_content_blocks_filters_empty_blocks() { @@ -111,32 +111,32 @@ public function test_resolve_content_blocks_filters_empty_blocks() { $actual = $this->instance->resolve_content_blocks( $post_model, [ 'flat' => true ] ); // There should return only the non-empty blocks $this->assertEquals( 6, count( $actual ) ); - $this->assertEquals( 'core/columns', $actual[0]['blockName'] ); + $this->assertEquals( 'core/columns', $actual[0]->name ); } public function test_resolve_content_blocks_resolves_classic_blocks() { $post_model = new Post( get_post( $this->post_id ) ); $actual = $this->instance->resolve_content_blocks( $post_model, [ 'flat' => true ] ); - $this->assertEquals( 'core/freeform', $actual[5]['blockName'] ); + $this->assertEquals( 'core/freeform', $actual[5]->name ); } public function test_resolve_content_blocks_filters_blocks_not_from_allow_list() { $post_model = new Post( get_post( $this->post_id ) ); $allowed = [ 'core/column', 'core/paragraph' ]; - $parsed_blocks = $this->instance->resolve_content_blocks( $post_model, [ 'flat' => true ], $allowed ); + $actual = $this->instance->resolve_content_blocks( $post_model, [ 'flat' => true ], $allowed ); $actual_block_names = array_values( array_unique( array_map( - static function ( $parsed_block ) { - return $parsed_block['blockName']; + static function ( $block ) { + return $block->name; }, - $parsed_blocks, + $actual, ) ) ); // There should return only blocks from the allow list - $this->assertEquals( 4, count( $parsed_blocks ) ); + $this->assertEquals( 4, count( $actual ) ); $this->assertEquals( $allowed, $actual_block_names ); } @@ -205,7 +205,7 @@ static function ( $blocks, $node, $args, $allowed_block_names ) { // The block should be resolved from the post node. $this->assertCount( 1, $resolved_blocks ); - $this->assertEquals( 'core/test-filter', $resolved_blocks[0]['blockName'] ); + $this->assertEquals( 'core/test-filter', $resolved_blocks[0]->name ); // Cleanup. remove_all_filters( 'wpgraphql_content_blocks_resolve_blocks' ); @@ -254,27 +254,27 @@ public function test_inner_blocks() { $resolved_blocks = $this->instance->resolve_content_blocks( $post, [ 'flat' => false ] ); $this->assertCount( 1, $resolved_blocks, 'There should be only one top-level block (columns).' ); - $this->assertEquals( 'core/columns', $resolved_blocks[0]['blockName'] ); - $this->assertNotEmpty( $resolved_blocks[0]['clientId'], 'The clientId should be set.' ); - $this->assertArrayNotHasKey( 'parentClientId', $resolved_blocks[0], 'The parentClientId should be empty.' ); + $this->assertEquals( 'core/columns', $resolved_blocks[0]->name, 'The top-level block should be columns.' ); + $this->assertNotEmpty( $resolved_blocks[0]->clientId, 'The clientId should be set.' ); + $this->assertEmpty( $resolved_blocks[0]->parentClientId, 'The parentClientId should be empty.' ); - $this->assertCount( 2, $resolved_blocks[0]['innerBlocks'], 'There should be two inner blocks (columns).' ); + $this->assertCount( 2, $resolved_blocks[0]->innerBlocks, 'There should be two inner blocks (columns).' ); // Check the inner blocks. - $expected_parent_client_id = $resolved_blocks[0]['clientId']; + $expected_parent_client_id = $resolved_blocks[0]->clientId; - foreach ( $resolved_blocks[0]['innerBlocks'] as $inner_block ) { - $this->assertEquals( 'core/column', $inner_block['blockName'] ); - $this->assertCount( 2, $inner_block['innerBlocks'], 'There should be two inner blocks (column).' ); - $this->assertNotEmpty( $inner_block['clientId'], 'The clientId should be set.' ); - $this->assertArrayNotHasKey( 'parentClientId', $resolved_blocks[0], 'The parentClientId should only be set when flattening.' ); // @todo This is incorrect, the parentClientId should be set for nested blocks. + foreach ( $resolved_blocks[0]->innerBlocks as $inner_block ) { + $this->assertEquals( 'core/column', $inner_block->name, 'The inner block should be a column.' ); + $this->assertCount( 2, $inner_block->innerBlocks, 'There should be two inner blocks (column).' ); + $this->assertNotEmpty( $inner_block->clientId, 'The clientId should be set.' ); + $this->assertEmpty( $resolved_blocks[0]->parentClientId, 'The parentClientId should only be set when flattening.' ); // @todo This is incorrect, the parentClientId should be set for nested blocks. // Check the inner inner blocks. - $expected_parent_client_id = $inner_block['clientId']; + $expected_parent_client_id = $inner_block->clientId; - foreach ( $inner_block['innerBlocks'] as $inner_inner_block ) { - $this->assertNotEmpty( $inner_inner_block['clientId'], 'The clientId should be set.' ); - $this->assertArrayNotHasKey( 'parentClientId', $resolved_blocks[0], 'The parentClientId should only be set when flattening.' ); // @todo This is incorrect, the parentClientId should be set for nested blocks. + foreach ( $inner_block->innerBlocks as $inner_inner_block ) { + $this->assertNotEmpty( $inner_inner_block->clientId, 'The clientId should be set.' ); + $this->assertEmpty( $resolved_blocks[0]->parentClientId, 'The parentClientId should only be set when flattening.' ); // @todo This is incorrect, the parentClientId should be set for nested blocks. } } @@ -287,40 +287,40 @@ public function test_inner_blocks() { $this->assertCount( 7, $resolved_blocks, 'There should be five blocks when flattened.' ); // Check the top-level block (columns). - $this->assertNotEmpty( $resolved_blocks[0]['clientId'], 'The clientId should be set.' ); - $this->assertEqualBlocks( $expected_blocks[0], $resolved_blocks[0], 'The top-level block should match.' ); + $this->assertNotEmpty( $resolved_blocks[0]->clientId, 'The clientId should be set.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->wpBlock->attributes, $resolved_blocks[0]->wpBlock->attributes, 'The top-level block should match.' ); // Check first inner block (column). - $expected_parent_client_id = $resolved_blocks[0]['clientId']; - $this->assertNotEmpty( $resolved_blocks[1]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[1]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][0], $resolved_blocks[1], 'The first inner block should match.' ); + $expected_parent_client_id = $resolved_blocks[0]->clientId; + $this->assertNotEmpty( $resolved_blocks[1]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[1]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[0]->wpBlock->attributes, $resolved_blocks[1]->wpBlock->attributes, 'The first inner block should match.' ); // Check first inner block children. - $expected_parent_client_id = $resolved_blocks[1]['clientId']; - $this->assertNotEmpty( $resolved_blocks[2]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[2]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][0]['innerBlocks'][0], $resolved_blocks[2], 'The first inner inner block should match.' ); + $expected_parent_client_id = $resolved_blocks[1]->clientId; + $this->assertNotEmpty( $resolved_blocks[2]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[2]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[0]->innerBlocks[0]->wpBlock->attributes, $resolved_blocks[2]->wpBlock->attributes, 'The first inner inner block should match.' ); - $this->assertNotEmpty( $resolved_blocks[3]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[3]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][0]['innerBlocks'][1], $resolved_blocks[3], 'The second inner inner block should match.' ); + $this->assertNotEmpty( $resolved_blocks[3]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[3]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[0]->innerBlocks[1]->wpBlock->attributes, $resolved_blocks[3]->wpBlock->attributes, 'The second inner inner block should match.' ); // Check second inner block (column). - $expected_parent_client_id = $resolved_blocks[0]['clientId']; - $this->assertNotEmpty( $resolved_blocks[4]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[4]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][1], $resolved_blocks[4], 'The first inner block should match.' ); + $expected_parent_client_id = $resolved_blocks[0]->clientId; + $this->assertNotEmpty( $resolved_blocks[4]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[4]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[1]->wpBlock->attributes, $resolved_blocks[4]->wpBlock->attributes, 'The first inner block should match.' ); // Check second inner block children. - $expected_parent_client_id = $resolved_blocks[4]['clientId']; - $this->assertNotEmpty( $resolved_blocks[5]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[5]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][1]['innerBlocks'][0], $resolved_blocks[5], 'The first inner inner block should match.' ); - - $this->assertNotEmpty( $resolved_blocks[6]['clientId'], 'The clientId should be set.' ); - $this->assertEquals( $expected_parent_client_id, $resolved_blocks[6]['parentClientId'], 'The parentClientId should match.' ); - $this->assertEqualBlocks( $expected_blocks[0]['innerBlocks'][1]['innerBlocks'][1], $resolved_blocks[6], 'The second inner inner block should match.' ); + $expected_parent_client_id = $resolved_blocks[4]->clientId; + $this->assertNotEmpty( $resolved_blocks[5]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[5]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[1]->innerBlocks[0]->wpBlock->attributes, $resolved_blocks[5]->wpBlock->attributes, 'The first inner inner block should match.' ); + + $this->assertNotEmpty( $resolved_blocks[6]->clientId, 'The clientId should be set.' ); + $this->assertEquals( $expected_parent_client_id, $resolved_blocks[6]->parentClientId, 'The parentClientId should match.' ); + $this->assertEqualBlockAttributes( $expected_blocks[0]->innerBlocks[1]->innerBlocks[1]->wpBlock->attributes, $resolved_blocks[6]->wpBlock->attributes, 'The second inner inner block should match.' ); } /** @@ -330,7 +330,7 @@ public function test_inner_blocks() { * @param array $actual The actual block. * @param string $message The message to display if the assertion fails. */ - protected function assertEqualBlocks( $expected, $actual, $message = '' ) { + protected function assertEqualBlockAttributes( $expected, $actual, $message = '' ) { // Remove clientId and parentClientId from comparison. unset( $expected['clientId'] ); unset( $expected['parentClientId'] ); From 653040c3aec6726dbd420b0a1ebdef3e13e989dd Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 18 Oct 2024 10:46:47 +0000 Subject: [PATCH 2/7] chore: lint --- includes/Blocks/Block.php | 2 +- includes/Model/Block.php | 11 ++--------- includes/Utilities/DOMHelpers.php | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/includes/Blocks/Block.php b/includes/Blocks/Block.php index 8a4272b2..e1d026a3 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -8,7 +8,7 @@ namespace WPGraphQL\ContentBlocks\Blocks; use WPGraphQL\ContentBlocks\Data\BlockAttributeResolver; -use \WPGraphQL\ContentBlocks\Model\Block as BlockModel; +use WPGraphQL\ContentBlocks\Model\Block as BlockModel; use WPGraphQL\ContentBlocks\Registry\Registry; use WPGraphQL\ContentBlocks\Type\Scalar\Scalar; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; diff --git a/includes/Model/Block.php b/includes/Model/Block.php index b2bf64c8..9e423b25 100644 --- a/includes/Model/Block.php +++ b/includes/Model/Block.php @@ -56,6 +56,7 @@ public function __construct( \WP_Block $block ) { if ( ! $this->data->block_type ) { graphql_debug( sprintf( + // translators: %s is the block name. __( 'Block type not found for block: %s', 'wp-graphql-content-blocks' ), $this->data->name ?: 'Unknown' ), @@ -154,14 +155,6 @@ protected function resolve_attributes(): array { protected function resolve_attribute( string $attribute_name ) { $attribute_config = $this->data->block_type->attributes[ $attribute_name ] ?? []; - return BlockAttributeResolver::resolve_block_attribute( $attribute_config, $this->get_rendered_block(), $this->data->attributes[ $attribute_name ] ); - - $allowed_types = [ 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ]; - // If attribute type is set and valid, sanitize value. - if ( isset( $attribute['type'] ) && in_array( $attribute_config['type'], $allowed_types, true ) && rest_validate_value_from_schema( $result, $attribute_config ) ) { - $result = rest_sanitize_value_from_schema( $result, $attribute_config ); - } - - return $result; + return BlockAttributeResolver::resolve_block_attribute( $attribute_config, $this->get_rendered_block() ?? '', $this->data->attributes[ $attribute_name ] ); } } diff --git a/includes/Utilities/DOMHelpers.php b/includes/Utilities/DOMHelpers.php index 4f76b879..82381fcb 100644 --- a/includes/Utilities/DOMHelpers.php +++ b/includes/Utilities/DOMHelpers.php @@ -37,7 +37,7 @@ public static function parse_attribute( string $html, string $selector, string $ $selector = '*'; } - $nodes = $doc->find( $selector ); + $nodes = $doc->find( $selector ); foreach ( $nodes as $node ) { if ( $node->hasAttribute( $attribute ) ) { From 30a64cc4aad37c16ae5dc563adfa81bd892b2a45 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 18 Oct 2024 10:51:39 +0000 Subject: [PATCH 3/7] tests: remove assertions for parse_attribute() default parameter --- tests/unit/DOMHelpersTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/DOMHelpersTest.php b/tests/unit/DOMHelpersTest.php index ba2abd4d..5a2cc325 100644 --- a/tests/unit/DOMHelpersTest.php +++ b/tests/unit/DOMHelpersTest.php @@ -21,7 +21,6 @@ public function testParseAttribute(): void { // $html $this->assertNull( DOMHelpers::parse_attribute( '', $no_existent_selector, $data_attribute ) ); $this->assertNull( DOMHelpers::parse_attribute( $html, $no_existent_selector, $data_attribute ) ); - $this->assertEquals( 'Bar', DOMHelpers::parse_attribute( $html, $no_existent_selector, $data_attribute, 'Bar' ) ); $this->assertEquals( 'foo-data', DOMHelpers::parse_attribute( $html, $id_selector, $data_attribute ) ); $this->assertEquals( 'foo-class', DOMHelpers::parse_attribute( $html, $id_selector, $class_attribute ) ); $this->assertEquals( 'foo-id', DOMHelpers::parse_attribute( $html, $id_selector, $id_attribute ) ); @@ -36,7 +35,6 @@ public function testParseAttribute(): void { $this->assertEquals( 'center', DOMHelpers::parse_attribute( $html2, '*', 'data-align' ) ); $this->assertEquals( 'right', DOMHelpers::parse_attribute( $html2, '.has-text-align-right', 'data-align' ) ); $this->assertNull( DOMHelpers::parse_attribute( $html2, '.non-existent-class', 'data-align' ) ); - $this->assertEquals( 'default', DOMHelpers::parse_attribute( $html2, '.non-existent-class', 'data-align', 'default' ) ); // $htm3 $this->assertEquals( 'left', DOMHelpers::parse_attribute( $html3, 'span', 'data-align' ) ); From 59ea04483161701403fe254164f00091059c61e3 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sun, 27 Oct 2024 01:48:08 +0300 Subject: [PATCH 4/7] dev: cleanup and hydrate more block types --- .phpcs.xml.dist | 2 +- includes/Blocks/Block.php | 48 ++++++-------- includes/Data/ContentBlocksResolver.php | 62 +++++++++++++++++-- includes/Model/Block.php | 45 -------------- .../InterfaceType/EditorBlockInterface.php | 1 + tests/unit/ContentBlocksResolverTest.php | 10 ++- 6 files changed, 86 insertions(+), 82 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 4cdec649..0840286b 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/Blocks/Block.php b/includes/Blocks/Block.php index e1d026a3..96aaa9d9 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -121,6 +121,7 @@ private function register_block_attributes_as_fields(): void { $this->type_name ), 'resolve' => static function ( $block ) { + // Use the model to resolve the block attributes. return $block; }, ] @@ -137,55 +138,41 @@ private function register_block_attributes_as_fields(): void { * @return mixed */ private function get_attribute_type( $name, $attribute, $prefix ) { - $type = null; - if ( isset( $attribute['type'] ) ) { switch ( $attribute['type'] ) { case 'rich-text': case 'string': - $type = 'String'; - break; + return 'String'; case 'boolean': - $type = 'Boolean'; - break; + return 'Boolean'; case 'number': - $type = 'Float'; - break; + return 'Float'; case 'integer': - $type = 'Int'; - break; + return 'Int'; case 'array': if ( isset( $attribute['query'] ) ) { - $type = [ 'list_of' => $this->get_query_type( $name, $attribute['query'], $prefix ) ]; - } elseif ( isset( $attribute['items'] ) ) { + return [ 'list_of' => $this->get_query_type( $name, $attribute['query'], $prefix ) ]; + } + + if ( isset( $attribute['items'] ) ) { $of_type = $this->get_attribute_type( $name, $attribute['items'], $prefix ); if ( null !== $of_type ) { - $type = [ 'list_of' => $of_type ]; - } else { - $type = Scalar::get_block_attributes_array_type_name(); + return [ 'list_of' => $of_type ]; } - } else { - $type = Scalar::get_block_attributes_array_type_name(); + + return Scalar::get_block_attributes_array_type_name(); } - break; + + return Scalar::get_block_attributes_array_type_name(); case 'object': - $type = Scalar::get_block_attributes_object_type_name(); - break; + return Scalar::get_block_attributes_object_type_name(); } } elseif ( isset( $attribute['source'] ) ) { - $type = 'String'; + return 'String'; } - if ( null !== $type ) { - $default_value = $attribute['default'] ?? null; - - if ( isset( $default_value ) ) { - $type = [ 'non_null' => $type ]; - } - } - - return $type; + return null; } /** @@ -221,6 +208,7 @@ private function get_block_attribute_fields( ?array $block_attributes, string $p $config = [ $attribute_name => $attribute_config, ]; + $result = $this->resolve_block_attributes_recursive( $block, $config ); // Normalize the value. diff --git a/includes/Data/ContentBlocksResolver.php b/includes/Data/ContentBlocksResolver.php index be717e48..abb0d82c 100644 --- a/includes/Data/ContentBlocksResolver.php +++ b/includes/Data/ContentBlocksResolver.php @@ -89,12 +89,29 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name $parsed_blocks = apply_filters( 'wpgraphql_content_blocks_resolve_blocks', $parsed_blocks, $node, $args, $allowed_block_names ); // Map the blocks to the Block model - return is_array( $parsed_blocks ) ? array_map( + $models = is_array( $parsed_blocks ) ? array_map( static function ( $parsed_block ) { - return new Block( new \WP_Block( $parsed_block ) ); + $wp_block = new \WP_Block( $parsed_block ); + + if ( ! $wp_block->block_type ) { + graphql_debug( + sprintf( + // translators: %s is the block name. + __( 'Block type not found for block: %s', 'wp-graphql-content-blocks' ), + $wp_block->name ?: 'Unknown' + ), + ); + + return null; + } + + return new Block( $wp_block ); }, $parsed_blocks ) : []; + + // Filter out unknown blocks. + return array_values( array_filter( $models ) ); } /** @@ -153,12 +170,19 @@ private static function handle_do_block( array $block ): ?array { // Assign a unique clientId to the block. $block['clientId'] = uniqid(); - // @todo apply more hydrations. + // Some block need to be hydrated. $block = self::populate_template_part_inner_blocks( $block ); + $block = self::populate_post_content_inner_blocks( $block ); $block = self::populate_reusable_blocks( $block ); - $block = self::populate_pattern_inner_blocks( $block ); + /** + * Filters the block data after it has been processed. + * + * @param array $block The block data. + */ + $block = apply_filters( 'wpgraphql_content_blocks_handle_do_block', $block ); + // Prepare innerBlocks. if ( ! empty( $block['innerBlocks'] ) ) { $block['innerBlocks'] = self::handle_do_blocks( $block['innerBlocks'] ); @@ -219,6 +243,35 @@ private static function populate_template_part_inner_blocks( array $block ): arr return $block; } + /** + * Populates the innerBlocks of a core/post-content block with the blocks from the post content. + * + * @param array $block The block to populate. + * + * @return array The populated block. + */ + private static function populate_post_content_inner_blocks( array $block ): array { + if ( 'core/post-content' !== $block['blockName'] ) { + return $block; + } + + $post = get_post(); + + if ( ! $post ) { + return $block; + } + + $parsed_blocks = ! empty( $post->post_content ) ? self::parse_blocks( $post->post_content ) : null; + + if ( empty( $parsed_blocks ) ) { + return $block; + } + + $block['innerBlocks'] = $parsed_blocks; + + return $block; + } + /** * Populates reusable blocks with the blocks from the reusable ref ID. * @@ -269,6 +322,7 @@ private static function populate_pattern_inner_blocks( array $block ): array { } $block['innerBlocks'] = $resolved_patterns; + return $block; } diff --git a/includes/Model/Block.php b/includes/Model/Block.php index 9e423b25..41d56677 100644 --- a/includes/Model/Block.php +++ b/includes/Model/Block.php @@ -9,7 +9,6 @@ namespace WPGraphQL\ContentBlocks\Model; -use WPGraphQL\ContentBlocks\Data\BlockAttributeResolver; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; use WPGraphQL\Model\Model; @@ -52,20 +51,6 @@ class Block extends Model { public function __construct( \WP_Block $block ) { $this->data = $block; - // Log a Debug message if the block type is not found. - if ( ! $this->data->block_type ) { - graphql_debug( - sprintf( - // translators: %s is the block name. - __( 'Block type not found for block: %s', 'wp-graphql-content-blocks' ), - $this->data->name ?: 'Unknown' - ), - [ - 'parsed_block' => $block, - ] - ); - } - parent::__construct(); } @@ -127,34 +112,4 @@ protected function get_rendered_block(): ?string { return $this->rendered_block; } - - /** - * Resolves the block attributes. - * - * @return array):mixed> - */ - protected function resolve_attributes(): array { - $registered_attributes = $this->data->block_type->attributes ?? []; - - $resolvers = []; - - foreach ( array_keys( $registered_attributes ) as $attribute_name ) { - $resolvers[ $attribute_name ] = fn () => $this->resolve_attribute( $attribute_name ); - } - - return $resolvers; - } - - /** - * Resolves a single block attribute. - * - * @param string $attribute_name The name of the attribute being resolved. - * - * @return mixed - */ - protected function resolve_attribute( string $attribute_name ) { - $attribute_config = $this->data->block_type->attributes[ $attribute_name ] ?? []; - - return BlockAttributeResolver::resolve_block_attribute( $attribute_config, $this->get_rendered_block() ?? '', $this->data->attributes[ $attribute_name ] ); - } } diff --git a/includes/Type/InterfaceType/EditorBlockInterface.php b/includes/Type/InterfaceType/EditorBlockInterface.php index cab03027..67091daa 100644 --- a/includes/Type/InterfaceType/EditorBlockInterface.php +++ b/includes/Type/InterfaceType/EditorBlockInterface.php @@ -9,6 +9,7 @@ use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; use WP_Block_Type_Registry; +use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; /** * Class EditorBlockInterface diff --git a/tests/unit/ContentBlocksResolverTest.php b/tests/unit/ContentBlocksResolverTest.php index 344e67cd..0ba53885 100644 --- a/tests/unit/ContentBlocksResolverTest.php +++ b/tests/unit/ContentBlocksResolverTest.php @@ -193,7 +193,13 @@ public function test_filters_wpgraphql_content_blocks_resolve_blocks() { add_filter( 'wpgraphql_content_blocks_resolve_blocks', static function ( $blocks, $node, $args, $allowed_block_names ) { - return [ [ 'blockName' => 'core/test-filter' ] ]; + return [ + [ 'blockName' => 'core/test-filter' ], // This will be filtered out. + [ + 'blockName' => 'core/paragraph', + 'attrs' => [ 'content' => 'Test content' ], + ] + ]; }, 10, 4 @@ -205,7 +211,7 @@ static function ( $blocks, $node, $args, $allowed_block_names ) { // The block should be resolved from the post node. $this->assertCount( 1, $resolved_blocks ); - $this->assertEquals( 'core/test-filter', $resolved_blocks[0]->name ); + $this->assertEquals( 'core/paragraph', $resolved_blocks[0]->name ); // Cleanup. remove_all_filters( 'wpgraphql_content_blocks_resolve_blocks' ); From 7f213f7c2b5cf5fbd31065290ee08177e7700106 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sun, 3 Nov 2024 00:52:26 +0200 Subject: [PATCH 5/7] feat: add Block Supports classes --- .../BlockSupports/AbstractBlockSupport.php | 36 +++++++++++ includes/BlockSupports/Align.php | 53 +++++++++++++++ includes/BlockSupports/Anchor.php | 64 +++++++++++++++++++ includes/BlockSupports/Color.php | 61 ++++++++++++++++++ includes/BlockSupports/CustomClassName.php | 53 +++++++++++++++ includes/BlockSupports/Shadow.php | 53 +++++++++++++++ includes/BlockSupports/Typography.php | 57 +++++++++++++++++ includes/Field/BlockSupports/Anchor.php | 14 ++-- includes/Registry/Registry.php | 43 +++++++++++-- 9 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 includes/BlockSupports/AbstractBlockSupport.php create mode 100644 includes/BlockSupports/Align.php create mode 100644 includes/BlockSupports/Anchor.php create mode 100644 includes/BlockSupports/Color.php create mode 100644 includes/BlockSupports/CustomClassName.php create mode 100644 includes/BlockSupports/Shadow.php create mode 100644 includes/BlockSupports/Typography.php diff --git a/includes/BlockSupports/AbstractBlockSupport.php b/includes/BlockSupports/AbstractBlockSupport.php new file mode 100644 index 00000000..a56ff9a9 --- /dev/null +++ b/includes/BlockSupports/AbstractBlockSupport.php @@ -0,0 +1,36 @@ + __( 'Attributes for a block with Align support', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'align' => [ + 'type' => 'String', + 'description' => __( 'The align attribute for the block.', 'wp-graphql-content-blocks' ), + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'align' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithAlignSupportAttributes' ]; + } +} diff --git a/includes/BlockSupports/Anchor.php b/includes/BlockSupports/Anchor.php new file mode 100644 index 00000000..24402b83 --- /dev/null +++ b/includes/BlockSupports/Anchor.php @@ -0,0 +1,64 @@ + __( 'Attributes for a Block with Anchor support.', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'anchor' => [ + 'type' => 'String', + 'description' => __( 'The anchor attribute for the block.', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + $rendered_block = wp_unslash( $block->renderedHtml ); + + if ( empty( $rendered_block ) ) { + return null; + } + + return DOMHelpers::parse_first_node_attribute( $rendered_block, 'id' ); + }, + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'anchor' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithAnchorSupportAttributes' ]; + } +} diff --git a/includes/BlockSupports/Color.php b/includes/BlockSupports/Color.php new file mode 100644 index 00000000..6590abec --- /dev/null +++ b/includes/BlockSupports/Color.php @@ -0,0 +1,61 @@ + __( 'Attributes for a Block with Color support.', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'backgroundColor' => [ + 'type' => 'String', + 'description' => __( 'The backgroundColor attribute for the block.', 'wp-graphql-content-blocks' ), + ], + 'textColor' => [ + 'type' => 'String', + 'description' => __( 'The textColor attribute for the block.', 'wp-graphql-content-blocks' ), + ], + 'gradient' => [ + 'type' => 'String', + 'description' => __( 'The gradientColor attribute for the block.', 'wp-graphql-content-blocks' ), + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'color' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithColorSupportAttributes' ]; + } +} diff --git a/includes/BlockSupports/CustomClassName.php b/includes/BlockSupports/CustomClassName.php new file mode 100644 index 00000000..cb4e0449 --- /dev/null +++ b/includes/BlockSupports/CustomClassName.php @@ -0,0 +1,53 @@ + __( 'Attributes for a block with customClassName support', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'className' => [ + 'type' => 'String', + 'description' => __( 'The custom CSS class name attribute for the block.', 'wp-graphql-content-blocks' ), + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'customClassName' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithCustomClassNameSupportAttributes' ]; + } +} diff --git a/includes/BlockSupports/Shadow.php b/includes/BlockSupports/Shadow.php new file mode 100644 index 00000000..d4e959e0 --- /dev/null +++ b/includes/BlockSupports/Shadow.php @@ -0,0 +1,53 @@ + __( 'Attributes for a Block with Shadow support.', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'shadow' => [ + 'type' => 'String', + 'description' => __( 'The shadow attribute for the block.', 'wp-graphql-content-blocks' ), + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'shadow' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithShadowSupportAttributes' ]; + } +} diff --git a/includes/BlockSupports/Typography.php b/includes/BlockSupports/Typography.php new file mode 100644 index 00000000..e1fddf80 --- /dev/null +++ b/includes/BlockSupports/Typography.php @@ -0,0 +1,57 @@ + __( 'Attributes for a Block with Typography support.', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'fontSize' => [ + 'type' => 'String', + 'description' => __( 'The fontSize attribute for the block.', 'wp-graphql-content-blocks' ), + ], + 'fontFamily' => [ + 'type' => 'String', + 'description' => __( 'The fontFamily attribute for the block.', 'wp-graphql-content-blocks' ), + ], + ], + ] + ); + } + + /** + * {@inheritDoc} + */ + public static function has_block_support( \WP_Block_Type $block_type ): bool { + return block_has_support( $block_type, [ 'typography' ], false ); + } + + /** + * {@inheritDoc} + */ + public static function get_attributes_interfaces( \WP_Block_Type $block_type ): array { + if ( ! self::has_block_support( $block_type ) ) { + return []; + } + + return [ 'BlockWithTypographySupportAttributes' ]; + } +} diff --git a/includes/Field/BlockSupports/Anchor.php b/includes/Field/BlockSupports/Anchor.php index 49ad3409..bab2af5b 100644 --- a/includes/Field/BlockSupports/Anchor.php +++ b/includes/Field/BlockSupports/Anchor.php @@ -2,6 +2,8 @@ /** * Registers the BlockSupports Anchor Field * + * @deprecated @next-version Use `BlockWithAnchorSupportAttributes` instead. + * * @package WPGraphQL\ContentBlocks\Field\BlockSupports */ @@ -21,12 +23,14 @@ public static function register(): void { register_graphql_interface_type( 'BlockWithSupportsAnchor', [ - 'description' => __( 'Block that supports Anchor field', 'wp-graphql-content-blocks' ), - 'fields' => [ + 'description' => __( 'Block that supports Anchor field', 'wp-graphql-content-blocks' ), + 'deprecationReason' => __( 'Use `BlockWithAnchorSupportAttributes` instead.', 'wp-graphql-content-blocks' ), + 'fields' => [ 'anchor' => [ - 'type' => 'string', - 'description' => __( 'The anchor field for the block.', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { + 'type' => 'string', + 'description' => __( 'The anchor field for the block.', 'wp-graphql-content-blocks' ), + 'deprecationReason' => __( 'Use `BlockWithAnchorSupportAttributes` instead.', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { $rendered_block = wp_unslash( $block->renderedHtml ); if ( empty( $rendered_block ) ) { diff --git a/includes/Registry/Registry.php b/includes/Registry/Registry.php index dacc69e7..13623154 100644 --- a/includes/Registry/Registry.php +++ b/includes/Registry/Registry.php @@ -7,8 +7,9 @@ namespace WPGraphQL\ContentBlocks\Registry; +use WPGraphQL\ContentBlocks\BlockSupports; use WPGraphQL\ContentBlocks\Blocks\Block; -use WPGraphQL\ContentBlocks\Field\BlockSupports\Anchor; +use WPGraphQL\ContentBlocks\Field\BlockSupports\Anchor as DeprecatedAnchorField; use WPGraphQL\ContentBlocks\Type\InterfaceType\EditorBlockInterface; use WPGraphQL\ContentBlocks\Type\InterfaceType\PostTypeBlockInterface; use WPGraphQL\ContentBlocks\Type\Scalar\Scalar; @@ -179,7 +180,7 @@ public function get_block_additional_interfaces( string $block_name ): array { $block_interfaces = []; // NOTE: Using add_filter here creates a performance penalty. - $block_interfaces = Anchor::get_block_interfaces( $block_interfaces, $block_spec ); + $block_interfaces = DeprecatedAnchorField::get_block_interfaces( $block_interfaces, $block_spec ); return $block_interfaces; } @@ -199,8 +200,21 @@ public function get_block_attributes_interfaces( string $block_name ): array { } $block_interfaces = []; + + $block_support_classes = $this->get_block_supports_classes(); + + foreach ( $block_support_classes as $instance ) { + $interfaces_to_add = $instance::get_attributes_interfaces( $block_spec ); + if ( empty( $interfaces_to_add ) ) { + continue; + } + + $block_interfaces[] = array_merge( $block_interfaces, $interfaces_to_add ); + } + // NOTE: Using add_filter here creates a performance penalty. - $block_interfaces = Anchor::get_block_attributes_interfaces( $block_interfaces, $block_spec ); + $block_interfaces = DeprecatedAnchorField::get_block_attributes_interfaces( $block_interfaces, $block_spec ); + return $block_interfaces; } @@ -312,6 +326,27 @@ protected function register_block_type( WP_Block_Type $block ): void { * @return void */ protected function register_support_block_types() { - Anchor::register(); + DeprecatedAnchorField::register(); + + $block_support_classes = $this->get_block_supports_classes(); + foreach ( $block_support_classes as $block_support_class ) { + $block_support_class::register(); + } + } + + /** + * Get the block supports classes, keyed by the GraphQL type name they register. + * + * @return class-string<\WPGraphQL\ContentBlocks\BlockSupports\AbstractBlockSupport>[] + */ + private function get_block_supports_classes(): array { + return [ + BlockSupports\Align::class, + BlockSupports\Anchor::class, + BlockSupports\CustomClassName::class, + BlockSupports\Color::class, + BlockSupports\Shadow::class, + BlockSupports\Typography::class, + ]; } } From be4fbef2d96c8133a345baad308836edd23c7639 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sun, 3 Nov 2024 19:03:37 +0200 Subject: [PATCH 6/7] fix: core-query bug cleanup --- includes/Data/ContentBlocksResolver.php | 2 +- includes/Model/Block.php | 3 +-- includes/Registry/Registry.php | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/Data/ContentBlocksResolver.php b/includes/Data/ContentBlocksResolver.php index abb0d82c..caf0b10b 100644 --- a/includes/Data/ContentBlocksResolver.php +++ b/includes/Data/ContentBlocksResolver.php @@ -141,7 +141,7 @@ private static function handle_do_blocks( array $blocks ): array { foreach ( $blocks as $block ) { $block_data = self::handle_do_block( $block ); - if ( $block_data ) { + if ( ! empty( $block_data ) ) { $parsed[] = $block_data; } } diff --git a/includes/Model/Block.php b/includes/Model/Block.php index 41d56677..cbcce93b 100644 --- a/includes/Model/Block.php +++ b/includes/Model/Block.php @@ -106,8 +106,7 @@ protected function init() { */ protected function get_rendered_block(): ?string { if ( ! isset( $this->rendered_block ) ) { - $rendered = $this->data->render(); - $this->rendered_block = do_shortcode( $rendered ); + $this->rendered_block = ! empty( $this->data->parsed_block ) ? render_block( $this->data->parsed_block ) : ''; } return $this->rendered_block; diff --git a/includes/Registry/Registry.php b/includes/Registry/Registry.php index 13623154..8fe8f51c 100644 --- a/includes/Registry/Registry.php +++ b/includes/Registry/Registry.php @@ -66,9 +66,9 @@ public function __construct( TypeRegistry $type_registry, $block_type_registry ) * Registry init procedure. */ public function init(): void { - $this->register_interface_types(); $this->register_scalar_types(); $this->register_support_block_types(); + $this->register_interface_types(); $this->register_block_types(); } @@ -200,16 +200,17 @@ public function get_block_attributes_interfaces( string $block_name ): array { } $block_interfaces = []; - + $block_support_classes = $this->get_block_supports_classes(); foreach ( $block_support_classes as $instance ) { $interfaces_to_add = $instance::get_attributes_interfaces( $block_spec ); + if ( empty( $interfaces_to_add ) ) { continue; } - $block_interfaces[] = array_merge( $block_interfaces, $interfaces_to_add ); + $block_interfaces = array_merge( $block_interfaces, $interfaces_to_add ); } // NOTE: Using add_filter here creates a performance penalty. From a2fd59318c5cb1d132b8785bfa35232d955d5946 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Thu, 26 Dec 2024 01:30:41 +0200 Subject: [PATCH 7/7] chore: post-rebase cleanup --- includes/Type/InterfaceType/EditorBlockInterface.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/Type/InterfaceType/EditorBlockInterface.php b/includes/Type/InterfaceType/EditorBlockInterface.php index 67091daa..907ebe3c 100644 --- a/includes/Type/InterfaceType/EditorBlockInterface.php +++ b/includes/Type/InterfaceType/EditorBlockInterface.php @@ -9,7 +9,6 @@ use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; use WP_Block_Type_Registry; -use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; /** * Class EditorBlockInterface @@ -117,7 +116,7 @@ public static function register_type(): void { 'type' => 'String', 'description' => __( 'The (GraphQL) type of the block', 'wp-graphql-content-blocks' ), 'resolve' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + return $block->type; }, ], ],