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/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/Blocks/Block.php b/includes/Blocks/Block.php index 94258282..96aaa9d9 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', @@ -119,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; }, ] @@ -135,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; } /** @@ -219,8 +208,10 @@ 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 +316,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 +331,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 +358,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..caf0b10b 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,30 @@ 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 + $models = is_array( $parsed_blocks ) ? array_map( + static function ( $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 ) ); } /** @@ -118,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; } } @@ -147,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'] ); @@ -213,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. * @@ -263,6 +322,7 @@ private static function populate_pattern_inner_blocks( array $block ): array { } $block['innerBlocks'] = $resolved_patterns; + return $block; } diff --git a/includes/Field/BlockSupports/Anchor.php b/includes/Field/BlockSupports/Anchor.php index 4ca1ba02..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,16 +23,20 @@ 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 ) { - $rendered_block = wp_unslash( render_block( $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 ) ) { 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..cbcce93b --- /dev/null +++ b/includes/Model/Block.php @@ -0,0 +1,114 @@ + $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; + + 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 ) ) { + $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 dacc69e7..8fe8f51c 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; @@ -65,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(); } @@ -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,22 @@ 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 +327,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, + ]; } } diff --git a/includes/Type/InterfaceType/EditorBlockInterface.php b/includes/Type/InterfaceType/EditorBlockInterface.php index 5aa8e760..907ebe3c 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,38 +103,25 @@ 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', '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; }, ], ], '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..82381fcb 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; @@ -38,8 +37,7 @@ public static function parse_attribute( string $html, string $selector, string $ $selector = '*'; } - $nodes = $doc->find( $selector ); - $default_value = isset( $default_value ) ? $default_value : null; + $nodes = $doc->find( $selector ); 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..0ba53885 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 ); } @@ -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]['blockName'] ); + $this->assertEquals( 'core/paragraph', $resolved_blocks[0]->name ); // Cleanup. remove_all_filters( 'wpgraphql_content_blocks_resolve_blocks' ); @@ -254,27 +260,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 +293,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 +336,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'] ); 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' ) );