diff --git a/README.md b/README.md index a53b4a41..e25d8aaf 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ This plugin is currently developed for use on WordPress sites hosted on the VIP - [Example Post](#example-post) - [`include`](#include) - [`exclude`](#exclude) -- [Code Filters](#code-filters) +- [Filters and actions](#filters-and-actions) - [GraphQL](#graphql-1) - [REST](#rest-1) - [`vip_block_data_api__rest_validate_post_id`](#vip_block_data_api__rest_validate_post_id) @@ -1177,7 +1177,7 @@ This query parameter cannot be used at the same time as [the `include` query par Note that custom block filter rules can also be created in code via [the `vip_block_data_api__allow_block` filter](#vip_block_data_api__allow_block). -## Code Filters +## Filters and actions ### GraphQL @@ -1190,7 +1190,7 @@ add_filter( 'vip_block_data_api__is_graphql_enabled', '__return_false', 10, 1 ); ### REST -These filters can be applied to limit access to the REST API and modify the output of parsed blocks. +These filters and actions can be applied to limit access to the REST API and modify the output of parsed blocks. ### `vip_block_data_api__rest_validate_post_id` @@ -1299,6 +1299,38 @@ Note that this filter is evaluated after the [`include`](#include) and [`exclude --- +### `vip_block_data_api__sourced_block_inner_blocks` + +Modify a block's inner blocks before they are recursively added to the result tree. + +```php +/** + * Filters a block's inner blocks before recursive iteration. + * + * @param array $inner_blocks An array of inner block (WP_Block) instances. + * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. + * @param int $post_id Post ID associated with the parsed block. + * @param array $block Result of parse_blocks() for this block. + */ +$inner_blocks = apply_filters( 'vip_block_data_api__sourced_block_inner_blocks', $inner_blocks, $block_name, $this->post_id, $block->parsed_block ); +``` + +This is useful if you want to add or remove inner blocks from the tree based on the parent block. Note that the inner blocks are WP_Block instances, not the associative arrays returned by `parse_blocks`. + +```php +add_filter( 'vip_block_data_api__sourced_block_inner_blocks', 'remove_gallery_inner_blocks', 10, 4 ); + +function remove_gallery_inner_blocks( $inner_blocks, $block_name, $post_id, $block ) { + if ( 'core/gallery' === $block_name ) { + return []; + } + + return $inner_blocks; +} +``` + +--- + ### `vip_block_data_api__sourced_block_result` Modify or add attributes to a block's output in the Block Data API. @@ -1309,11 +1341,11 @@ Modify or add attributes to a block's output in the Block Data API. * * @param array $sourced_block An associative array of parsed block data with keys 'name' and 'attributes'. * @param string $block_name The name of the parsed block, e.g. 'core/paragraph'. - * @param string $post_id The post ID associated with the parsed block. - * @param string $block The result of parse_blocks() for this block. + * @param int $post_id The post ID associated with the parsed block. + * @param array $block The result of parse_blocks() for this block. * Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys. */ -$sourced_block = apply_filters( 'vip_block_data_api__sourced_block_result', $sourced_block, $block_name, $post_id, $block); +$sourced_block = apply_filters( 'vip_block_data_api__sourced_block_result', $sourced_block, $block_name, $post_id, $block->parsed_block); ``` This is useful when block rendering requires attributes stored in post metadata or outside of a block's markup. This filter can be used to add attributes to any core or custom block. For example: @@ -1408,7 +1440,7 @@ $result = apply_filters( 'vip_block_data_api__after_parse_blocks', $result, $pos This filter is called directly before returning a result in the REST API. Use this filter to add additional metadata or debug information to the API output. ```php -add_action( 'vip_block_data_api__after_parse_blocks', 'add_block_data_debug_info', 10, 2 ); +add_filter( 'vip_block_data_api__after_parse_blocks', 'add_block_data_debug_info', 10, 2 ); function add_block_data_debug_info( $result, $post_id ) { $result['debug']['my-value'] = 123; @@ -1430,6 +1462,33 @@ This would add `debug.my-value` to all Block Data API REST results: } ``` +--- + +### `vip_block_data_api__before_block_render` +### `vip_block_data_api__after_block_render` + +Perform actions before or after blocks are rendered by the `ContentParser`, such as hooking into core block rendering functions. + +```php +add_action( 'vip_block_data_api__before_block_render', 'add_block_context_filter', 10, 2 ); +add_action( 'vip_block_data_api__after_block_render', 'remove_block_context_filter', 10, 2 ); + +function block_context_filter( $block_context, $parsed_block ) { + // Modify block context before rendering + $block_context['custom/injected-context'] = 'example'; + + return $block_context; +} + +function add_block_context_filter( $blocks, $post_id ) { + add_filter( 'render_block_context', 'block_context_filter', 10, 2 ); +} + +function remove_block_context_filter( $blocks, $post_id ) { + remove_filter( 'render_block_context', 'block_context_filter', 10 ); +} +``` + ## Analytics **Please note that, this is for VIP sites only. Analytics are disabled if this plugin is not being run on VIP sites.** diff --git a/phpcs.xml.dist b/phpcs.xml.dist index f5ea0774..d7f04d55 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -46,7 +46,7 @@ - + diff --git a/src/parser/block-additions/core-block.php b/src/parser/block-additions/core-block.php new file mode 100644 index 00000000..14d50922 --- /dev/null +++ b/src/parser/block-additions/core-block.php @@ -0,0 +1,218 @@ +name ) { + $store_key = self::get_store_key( $parsed_block ); + if ( isset( self::$captured_inner_blocks[ $store_key ] ) ) { + self::$captured_inner_blocks[ $store_key ]['locked'] = true; + } + } + + // Get the parent block that is currently being rendered. This is fragile, + // but is currently the only way we can get access to the parent block from + // inside a dynamic block's render callback function. + // + // https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/class-wp-block.php#L517 + $parent_block = isset( WP_Block_Supports::$block_to_render ) ? WP_Block_Supports::$block_to_render : []; + + // If the parent block is not a synced pattern, do nothing. + if ( ! isset( $parent_block['attrs']['ref'] ) || self::$block_name !== $parent_block['blockName'] ) { + return $block_content; + } + + // Capture the inner block for this synced pattern. + self::capture_inner_block( $parent_block, $block ); + + return $block_content; + } + + /** + * Get captured inner blocks for synced patterns. Intended for use with + * the `vip_block_data_api__sourced_block_inner_blocks` filter. + * + * @param array $inner_blocks Inner blocks. + * @param string $block_name Block name. + * @param int|null $_post_id Post ID (unused). + * @param array $parsed_block Parsed block data. + * @return array + */ + public static function get_inner_blocks( array $inner_blocks, string $block_name, int|null $_post_id, array $parsed_block ): array { + if ( self::$block_name !== $block_name || ! isset( $parsed_block['attrs']['ref'] ) ) { + return $inner_blocks; + } + + $store_key = self::get_store_key( $parsed_block ); + + if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) { + return $inner_blocks; + } + + return self::$captured_inner_blocks[ $store_key ]['inner_blocks']; + } + + /** + * Create a unique key that can be used to identify a synced pattern. This + * allows us to store and retrieve inner blocks for synced patterns and avoid + * duplication when they are used multiple times within the same tree. + * + * Using a hash of attributes is important because they may contain synced + * pattern overrides, which can change the inner block content. The attributes + * contain the synced pattern post ID, so uniqueness is built-in. + * + * @param array $parsed_block Parsed block data. + * @return string + */ + protected static function get_store_key( array $parsed_block ): string { + // Include the synced pattern ID in the key just for legibility. + $synced_pattern_id = $parsed_block['attrs']['ref'] ?? null; + $attribute_json = wp_json_encode( $parsed_block['attrs'] ); + + return sprintf( '%s_%s', strval( $synced_pattern_id ), sha1( $attribute_json ) ); + } + + /** + * Capture inner block for a synced pattern. + * + * @param array $synced_pattern Synced pattern block (parsed block). + * @param WP_Block $block Inner block. + */ + protected static function capture_inner_block( array $synced_pattern, WP_Block $block ): void { + $store_key = self::get_store_key( $synced_pattern ); + if ( ! isset( self::$captured_inner_blocks[ $store_key ] ) ) { + self::$captured_inner_blocks[ $store_key ] = [ + 'inner_blocks' => [], + 'locked' => false, + ]; + } + + // This pattern has already been rendered somewhere in the tree and is now locked. + if ( self::$captured_inner_blocks[ $store_key ]['locked'] ) { + return; + } + + self::$captured_inner_blocks[ $store_key ]['inner_blocks'][] = $block; + } + + /** + * Remove the empty array that gets assigned to the content attribute due to + * this bug / side effect in the code that implements synced pattern overrides: + * + * phpcs:disable Generic.Commenting.DocComment.LongNotCapital + * https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L73 + * + * @param array $sourced_block Sourced block result. + * @param string $block_name Block name. + * @return array + */ + public static function remove_content_array( array $sourced_block, string $block_name ): array { + if ( self::$block_name !== $block_name ) { + return $sourced_block; + } + + // If the content attribute is set to an empty array, remove it. + $content = $sourced_block['attributes']['content'] ?? null; + if ( is_array( $content ) && empty( $content ) ) { + unset( $sourced_block['attributes']['content'] ); + } + + return $sourced_block; + } +} + +CoreBlock::init(); diff --git a/src/parser/content-parser.php b/src/parser/content-parser.php index 7d26b950..3ee12116 100644 --- a/src/parser/content-parser.php +++ b/src/parser/content-parser.php @@ -11,9 +11,12 @@ use Throwable; use WP_Error; -use WP_Block_Type; +use WP_Block; use WP_Block_Type_Registry; use Symfony\Component\DomCrawler\Crawler; +use function apply_filters; +use function do_action; +use function parse_blocks; /** * The content parser that would be used to transform a post into an array of blocks, along with their attributes. @@ -30,11 +33,11 @@ class ContentParser { /** * Post ID * - * @var int + * @var int|null * * @access private */ - protected $post_id; + protected $post_id = null; /** * Warnings that would be returned with the blocks * @@ -61,21 +64,28 @@ public function __construct( $block_registry = null ) { * Filter out a block from the blocks output based on: * * - include parameter, if it is set or - * - exclude parameter, if it is set. + * - exclude parameter, if it is set or + * - whether it is an empty whitespace block * * and finally, based on a filter vip_block_data_api__allow_block * - * @param array $block Current block. - * @param string $block_name Name of the block. - * @param array $filter_options Options to be used for filtering, if any. + * @param WP_Block $block Current block. + * @param array $filter_options Options to be used for filtering, if any. * * @return bool true, if the block should be included or false otherwise * * @access private */ - public function should_block_be_included( $block, $block_name, $filter_options ) { + protected function should_block_be_included( WP_Block $block, array $filter_options ) { + $block_name = $block->name; $is_block_included = true; + // Whitespace blocks are always excluded. + $is_whitespace_block = null === $block_name && empty( trim( $block->inner_html ) ); + if ( $is_whitespace_block ) { + return false; + } + if ( ! empty( $filter_options['include'] ) ) { $is_block_included = in_array( $block_name, $filter_options['include'] ); } elseif ( ! empty( $filter_options['exclude'] ) ) { @@ -86,16 +96,18 @@ public function should_block_be_included( $block, $block_name, $filter_options ) * Filter out blocks from the blocks output * * @param bool $is_block_included True if the block should be included, or false to filter it out. - * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. - * @param string $block Result of parse_blocks() for this block. + * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. + * @param array $block Result of parse_blocks() for this block. * Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys. */ - return apply_filters( 'vip_block_data_api__allow_block', $is_block_included, $block_name, $block ); + return apply_filters( 'vip_block_data_api__allow_block', $is_block_included, $block_name, $block->parsed_block ); } /** * Parses a post's content and returns an array of blocks with their attributes and inner blocks. * + * @global WP_Post $post + * * @param string $post_content HTML content of a post. * @param int|null $post_id ID of the post being parsed. Required for blocks containing meta-sourced attributes and some block filters. * @param array $filter_options An associative array of options for filtering blocks. Can contain keys: @@ -105,13 +117,33 @@ public function should_block_be_included( $block, $block_name, $filter_options ) * @return array|WP_Error */ public function parse( $post_content, $post_id = null, $filter_options = [] ) { + global $post; + Analytics::record_usage(); if ( isset( $filter_options['exclude'] ) && isset( $filter_options['include'] ) ) { return new WP_Error( 'vip-block-data-api-invalid-params', 'Cannot provide blocks to exclude and include at the same time', [ 'status' => 400 ] ); } - $this->post_id = $post_id; + // Temporarily set global $post. This is necessary to provide the built-in + // 'postId' and 'postType' contexts within synced patterns, which can be + // consumed by block bindings inside those patterns. + // + // https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks.php#L2025-L2035 + // + // For blocks outside of synced patterns, we provide this context ourselves + // in the render_parsed_block() method of this class, but synced patterns + // are essentially mini-block-tree islands that are rendered in isolation + // via `do_blocks`. + // + // See also: SyncedPatternsTest::test_multiple_nested_synced_patterns_with_block_bindings() + $previous_global_post = $post; + if ( is_int( $post_id ) ) { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post = get_post( $post_id ); + $this->post_id = $post_id; + } + $this->warnings = []; $has_blocks = has_blocks( $post_content ); @@ -137,19 +169,41 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { $post_content = apply_filters( 'vip_block_data_api__before_parse_post_content', $post_content, $post_id ); $blocks = parse_blocks( $post_content ); - $blocks = array_values( array_filter( $blocks, function ( $block ) { - $is_whitespace_block = ( null === $block['blockName'] && empty( trim( $block['innerHTML'] ) ) ); - return ! $is_whitespace_block; - } ) ); - $registered_blocks = $this->block_registry->get_all_registered(); + /** + * Fires before blocks are rendered, allowing code to hook into the block rendering process. + * + * @param array $blocks Blocks being rendered. + * @param int|null $post_id Post ID associated with the blocks. + * + * @since 1.4.0 + */ + do_action( 'vip_block_data_api__before_block_render', $blocks, $post_id ); + + $sourced_blocks = array_map( function ( $block ) use ( $filter_options ) { + // Render the block, then walk the tree using source_block to apply our + // sourced attribute logic. + $rendered_block = $this->render_parsed_block( $block ); - $sourced_blocks = array_map(function ( $block ) use ( $registered_blocks, $filter_options ) { - return $this->source_block( $block, $registered_blocks, $filter_options ); - }, $blocks); + return $this->source_block( $rendered_block, $filter_options ); + }, $blocks ); $sourced_blocks = array_values( array_filter( $sourced_blocks ) ); + /** + * Fires after block are rendered, allowing code to hook into the block rendering process. + * + * @param array $sourced_blocks Raw render result. + * @param int|null $post_id Post ID associated with the blocks. + * + * @since 1.4.0 + */ + do_action( 'vip_block_data_api__after_block_render', $sourced_blocks, $post_id ); + + // Restore global $post. + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post = $previous_global_post; + $result = [ 'blocks' => $sourced_blocks, ]; @@ -189,37 +243,120 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { } } + /** + * Helper function to render a parsed block, so that we can benefit from + * core-powered functions like block bindings and synced patterns. + * + * This loosely mirrors the code in the `render_block` function in core, but + * allows us to capture the block instance so that we can traverse the tree: + * + * https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks.php#L1959 + * + * @param array $parsed_block Parsed block (result of `parse_blocks`). + * @return WP_Block + */ + protected function render_parsed_block( array $parsed_block ): WP_Block { + $context = []; + if ( is_int( $this->post_id ) ) { + $context['postId'] = $this->post_id; + $context['postType'] = get_post_type( $this->post_id ); + } + + $context = apply_filters( 'render_block_context', $context, $parsed_block, null ); + + $block_instance = new WP_Block( $parsed_block, $context, $this->block_registry ); + $block_instance->render(); + + return $block_instance; + } + /** * Processes a single block, and returns the sourced block data. * - * @param array $block Block to be processed. - * @param WP_Block_Type[] $registered_blocks Blocks that have been registered. - * @param array $filter_options Options to filter using, if any. + * @param WP_Block $block Block to be processed. + * @param array $filter_options Options to filter using, if any. * * @return array|null * * @access private */ - protected function source_block( $block, $registered_blocks, $filter_options ) { - $block_name = $block['blockName']; + protected function source_block( WP_Block $block, array $filter_options ) { + $block_name = $block->name; - if ( ! $this->should_block_be_included( $block, $block_name, $filter_options ) ) { + if ( ! $this->should_block_be_included( $block, $filter_options ) ) { return null; } - if ( ! isset( $registered_blocks[ $block_name ] ) ) { + if ( ! $this->block_registry->is_registered( $block_name ) ) { $this->add_missing_block_warning( $block_name ); } - $block_definition = $registered_blocks[ $block_name ] ?? null; - $block_definition_attributes = $block_definition->attributes ?? []; + $sourced_block = [ + 'name' => $block->name, + 'attributes' => $this->apply_sourced_attributes( $block ), + ]; + + // WP_Block#inner_blocks can be an array or WP_Block_List (iterable). + $inner_blocks = iterator_to_array( $block->inner_blocks ); + + /** + * Filters a block's inner blocks before recursive iteration. + * + * @param array $inner_blocks An array of inner block (WP_Block) instances. + * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. + * @param int $post_id Post ID associated with the parsed block. + * @param array $block Result of parse_blocks() for this block. + */ + $inner_blocks = apply_filters( 'vip_block_data_api__sourced_block_inner_blocks', $inner_blocks, $block_name, $this->post_id, $block->parsed_block ); + + // Recursively iterate over inner blocks. + $sourced_inner_blocks = array_values( array_filter( array_map( function ( $inner_block ) use ( $filter_options ) { + return $this->source_block( $inner_block, $filter_options ); + }, $inner_blocks ) ) ); - $block_attributes = $block['attrs']; + // Only set innerBlocks if entries are present to match prior version behavior. + if ( ! empty( $sourced_inner_blocks ) ) { + $sourced_block['innerBlocks'] = $sourced_inner_blocks; + } + + /** + * Filters a block when parsing is complete. + * + * @param array $sourced_block An associative array of parsed block data with keys 'name' and 'attribute'. + * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. + * @param int $post_id Post ID associated with the parsed block. + * @param array $block Result of parse_blocks() for this block. Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys. + */ + $sourced_block = apply_filters( 'vip_block_data_api__sourced_block_result', $sourced_block, $block_name, $this->post_id, $block->parsed_block ); + + // If attributes are empty, explicitly use an object to avoid encoding an empty array in JSON. + if ( empty( $sourced_block['attributes'] ) ) { + $sourced_block['attributes'] = (object) []; + } + + return $sourced_block; + } + + /** + * Source the attributes of a block and return a merged attribute array. + * + * @param WP_Block $block Block to be processed. + * @return array Attribute array + */ + protected function apply_sourced_attributes( WP_Block $block ): array { + $block_definition = $this->block_registry->get_registered( $block->name ) ?? null; + $block_definition_attributes = $block_definition->attributes ?? []; + $block_attributes = $block->attributes; foreach ( $block_definition_attributes as $block_attribute_name => $block_attribute_definition ) { $attribute_source = $block_attribute_definition['source'] ?? null; $attribute_default_value = $block_attribute_definition['default'] ?? null; + // If the attribute was resolved from a block binding, skip. + if ( $this->has_successful_block_binding( $block_attribute_name, $block_attributes, $attribute_default_value ) ) { + continue; + } + if ( null === $attribute_source ) { // Unsourced attributes are stored in the block's delimiter attributes, skip DOM parser. @@ -237,7 +374,7 @@ protected function source_block( $block, $registered_blocks, $filter_options ) { } // Specify a manual doctype so that the parser will use the HTML5 parser. - $crawler = new Crawler( sprintf( '%s', $block['innerHTML'] ) ); + $crawler = new Crawler( sprintf( '%s', $block->inner_html ) ); // Enter the tag for block parsing. $crawler = $crawler->filter( 'body' )->children(); @@ -249,45 +386,35 @@ protected function source_block( $block, $registered_blocks, $filter_options ) { } } - $sourced_block = [ - 'name' => $block_name, - 'attributes' => $block_attributes, - ]; - - if ( isset( $block['innerBlocks'] ) ) { - $inner_blocks = array_map( function ( $block ) use ( $registered_blocks, $filter_options ) { - return $this->source_block( $block, $registered_blocks, $filter_options ); - }, $block['innerBlocks'] ); - - $inner_blocks = array_values( array_filter( $inner_blocks ) ); + // Sort attributes by key to ensure consistent output. + ksort( $block_attributes ); - if ( ! empty( $inner_blocks ) ) { - $sourced_block['innerBlocks'] = $inner_blocks; - } - } + return $block_attributes; + } - if ( $this->is_debug_enabled() ) { - $sourced_block['debug'] = [ - 'block_definition_attributes' => $block_definition->attributes, - ]; + /** + * Inspect the attribute to determine if it was resolved from a block binding. + * + * @param string $attribute_name Attribute name. + * @param array $attributes Block attributes. + * @param mixed $default_value Default value of the attribute. + */ + protected function has_successful_block_binding( string $attribute_name, array $attributes, mixed $default_value ): bool { + // No bindings defined. + if ( ! isset( $attributes['metadata']['bindings'] ) ) { + return false; } - /** - * Filters a block when parsing is complete. - * - * @param array $sourced_block An associative array of parsed block data with keys 'name' and 'attribute'. - * @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. - * @param int $post_id Post ID associated with the parsed block. - * @param array $block Result of parse_blocks() for this block. Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys. - */ - $sourced_block = apply_filters( 'vip_block_data_api__sourced_block_result', $sourced_block, $block_name, $this->post_id, $block ); + $attribute_value = $attributes[ $attribute_name ] ?? null; + $bindings = $attributes['metadata']['bindings']; - // If attributes are empty, explicitly use an object to avoid encoding an empty array in JSON. - if ( empty( $sourced_block['attributes'] ) ) { - $sourced_block['attributes'] = (object) []; + // If the attribute is empty or matches the default value, it was not resolved + // from a block binding. + if ( empty( $attribute_value ) || $attribute_value === $default_value ) { + return false; } - return $sourced_block; + return isset( $bindings[ $attribute_name ]['source'] ) || isset( $bindings['__default']['source'] ); } /** diff --git a/tests/graphql/test-graphql-api-v1.php b/tests/graphql/test-graphql-api-v1.php index 9a5d74f2..5380e96c 100644 --- a/tests/graphql/test-graphql-api-v1.php +++ b/tests/graphql/test-graphql-api-v1.php @@ -37,7 +37,7 @@ public function test_is_graphql_enabled_false() { // get_blocks_data() tests public function test_get_blocks_data() { - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -53,7 +53,7 @@ public function test_get_blocks_data() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-quote', [ + $this->register_block_with_attributes( 'test/custom-quote', [ 'value' => [ 'type' => 'string', 'source' => 'html', @@ -70,7 +70,7 @@ public function test_get_blocks_data() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-heading', [ + $this->register_block_with_attributes( 'test/custom-heading', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -192,7 +192,7 @@ public function test_get_blocks_data() { // get_blocks_data() attribute type tests public function test_array_data_in_attribute() { - $this->register_global_block_with_attributes( 'test/custom-table', [ + $this->register_block_with_attributes( 'test/custom-table', [ 'head' => [ 'type' => 'array', 'default' => [], @@ -306,11 +306,6 @@ public function test_array_data_in_attribute() { [ 'name' => 'test/custom-table', 'attributes' => [ - [ - 'name' => 'head', - 'value' => '[{"cells":[{"content":"Header A","tag":"th"},{"content":"Header B","tag":"th"}]}]', - 'isValueJsonEncoded' => true, - ], [ 'name' => 'body', 'value' => '[{"cells":[{"content":"Value A","tag":"td"},{"content":"Value B","tag":"td"}]},{"cells":[{"content":"Value C","tag":"td"},{"content":"Value D","tag":"td"}]}]', @@ -321,6 +316,11 @@ public function test_array_data_in_attribute() { 'value' => '[{"cells":[{"content":"Footer A","tag":"td"},{"content":"Footer B","tag":"td"}]}]', 'isValueJsonEncoded' => true, ], + [ + 'name' => 'head', + 'value' => '[{"cells":[{"content":"Header A","tag":"th"},{"content":"Header B","tag":"th"}]}]', + 'isValueJsonEncoded' => true, + ], ], 'id' => '1', ], diff --git a/tests/graphql/test-graphql-api-v2.php b/tests/graphql/test-graphql-api-v2.php index 326c35eb..53e5e2c1 100644 --- a/tests/graphql/test-graphql-api-v2.php +++ b/tests/graphql/test-graphql-api-v2.php @@ -37,7 +37,7 @@ public function test_is_graphql_enabled_false() { // get_blocks_data() tests public function test_get_blocks_data() { - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -53,7 +53,7 @@ public function test_get_blocks_data() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-quote', [ + $this->register_block_with_attributes( 'test/custom-quote', [ 'value' => [ 'type' => 'string', 'source' => 'html', @@ -70,7 +70,7 @@ public function test_get_blocks_data() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-heading', [ + $this->register_block_with_attributes( 'test/custom-heading', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -193,7 +193,7 @@ public function test_get_blocks_data() { // get_blocks_data() attribute type tests public function test_array_data_in_attribute() { - $this->register_global_block_with_attributes( 'test/custom-table', [ + $this->register_block_with_attributes( 'test/custom-table', [ 'head' => [ 'type' => 'array', 'default' => [], @@ -309,11 +309,6 @@ public function test_array_data_in_attribute() { 'id' => '1', 'parentId' => null, 'attributes' => [ - [ - 'name' => 'head', - 'value' => '[{"cells":[{"content":"Header A","tag":"th"},{"content":"Header B","tag":"th"}]}]', - 'isValueJsonEncoded' => true, - ], [ 'name' => 'body', 'value' => '[{"cells":[{"content":"Value A","tag":"td"},{"content":"Value B","tag":"td"}]},{"cells":[{"content":"Value C","tag":"td"},{"content":"Value D","tag":"td"}]}]', @@ -324,6 +319,11 @@ public function test_array_data_in_attribute() { 'value' => '[{"cells":[{"content":"Footer A","tag":"td"},{"content":"Footer B","tag":"td"}]}]', 'isValueJsonEncoded' => true, ], + [ + 'name' => 'head', + 'value' => '[{"cells":[{"content":"Header A","tag":"th"},{"content":"Header B","tag":"th"}]}]', + 'isValueJsonEncoded' => true, + ], ], ], ], @@ -339,7 +339,7 @@ public function test_array_data_in_attribute() { } public function test_get_block_data_with_boolean_attributes() { - $this->register_global_block_with_attributes( 'test/toggle-text', [ + $this->register_block_with_attributes( 'test/toggle-text', [ 'isVisible' => [ 'type' => 'boolean', ], @@ -362,13 +362,13 @@ public function test_get_block_data_with_boolean_attributes() { 'name' => 'test/toggle-text', 'attributes' => [ [ - 'name' => 'isVisible', - 'value' => 'true', + 'name' => 'isBordered', + 'value' => 'false', 'isValueJsonEncoded' => true, ], [ - 'name' => 'isBordered', - 'value' => 'false', + 'name' => 'isVisible', + 'value' => 'true', 'isValueJsonEncoded' => true, ], ], @@ -386,7 +386,7 @@ public function test_get_block_data_with_boolean_attributes() { } public function test_get_block_data_with_number_attributes() { - $this->register_global_block_with_attributes( 'test/gallery-block', [ + $this->register_block_with_attributes( 'test/gallery-block', [ 'tileCount' => [ 'type' => 'number', ], @@ -417,13 +417,13 @@ public function test_get_block_data_with_number_attributes() { 'isValueJsonEncoded' => true, ], [ - 'name' => 'tileWidthPx', - 'value' => '300', + 'name' => 'tileOpacity', + 'value' => '0.5', 'isValueJsonEncoded' => true, ], [ - 'name' => 'tileOpacity', - 'value' => '0.5', + 'name' => 'tileWidthPx', + 'value' => '300', 'isValueJsonEncoded' => true, ], ], @@ -441,7 +441,7 @@ public function test_get_block_data_with_number_attributes() { } public function test_get_block_data_with_string_attribute() { - $this->register_global_block_with_attributes( 'test/custom-block', [ + $this->register_block_with_attributes( 'test/custom-block', [ 'myComment' => [ 'type' => 'string', ], diff --git a/tests/parser/blocks/test-unregistered-block.php b/tests/parser/blocks/test-unregistered-block.php index e884d37d..2cc5a39a 100644 --- a/tests/parser/blocks/test-unregistered-block.php +++ b/tests/parser/blocks/test-unregistered-block.php @@ -31,7 +31,7 @@ public function test_parse_unregistered_block() { 'Block type "test/unknown-block" is not server-side registered. Sourced block attributes will not be available.', ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArrayHasKey( 'warnings', $blocks, sprintf( 'Expected parser to have warnings, none received: %s', wp_json_encode( $blocks ) ) ); diff --git a/tests/parser/sources/test-source-attribute.php b/tests/parser/sources/test-source-attribute.php index 5214695a..2273127b 100644 --- a/tests/parser/sources/test-source-attribute.php +++ b/tests/parser/sources/test-source-attribute.php @@ -37,7 +37,7 @@ public function test_parse_attribute_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -69,7 +69,7 @@ public function test_parse_attribute_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -100,7 +100,7 @@ public function test_parse_attribute_source__with_asterisk_selector() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-children.php b/tests/parser/sources/test-source-children.php index d5a5506e..198fd61f 100644 --- a/tests/parser/sources/test-source-children.php +++ b/tests/parser/sources/test-source-children.php @@ -52,7 +52,7 @@ public function test_parse_children__with_list_elements() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -86,7 +86,7 @@ public function test_parse_children__with_single_child() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -126,7 +126,7 @@ public function test_parse_children__with_mixed_nodes_and_text() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -157,7 +157,7 @@ public function test_parse_children__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-delimiter.php b/tests/parser/sources/test-source-delimiter.php index 5ee0b5fe..93e08cf2 100644 --- a/tests/parser/sources/test-source-delimiter.php +++ b/tests/parser/sources/test-source-delimiter.php @@ -45,7 +45,7 @@ public function test_parse_block_delimiter_attributes() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -75,7 +75,7 @@ public function test_parse_block_delimiter_attributes__are_overridden_by_sourced ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-html.php b/tests/parser/sources/test-source-html.php index 3b5ef245..61d6d87c 100644 --- a/tests/parser/sources/test-source-html.php +++ b/tests/parser/sources/test-source-html.php @@ -36,7 +36,7 @@ public function test_parse_html_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -71,7 +71,7 @@ public function test_parse_html_source__with_multiline_selector() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -102,7 +102,7 @@ public function test_parse_html_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-meta.php b/tests/parser/sources/test-source-meta.php index 972fd3e1..758d6704 100644 --- a/tests/parser/sources/test-source-meta.php +++ b/tests/parser/sources/test-source-meta.php @@ -39,7 +39,7 @@ public function test_parse_meta_source() { return $post_id; }; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html, $post_id ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); @@ -69,7 +69,7 @@ public function test_parse_meta_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html, $post_id ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); diff --git a/tests/parser/sources/test-source-node.php b/tests/parser/sources/test-source-node.php index 602532e7..875277ea 100644 --- a/tests/parser/sources/test-source-node.php +++ b/tests/parser/sources/test-source-node.php @@ -43,7 +43,7 @@ public function test_parse_node__with_object_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-query.php b/tests/parser/sources/test-source-query.php index 42b9263c..58548c1c 100644 --- a/tests/parser/sources/test-source-query.php +++ b/tests/parser/sources/test-source-query.php @@ -59,7 +59,7 @@ public function test_parse_query_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); @@ -145,7 +145,7 @@ public function test_parse_query_source__with_nested_query() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); @@ -193,7 +193,7 @@ public function test_parse_query_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); diff --git a/tests/parser/sources/test-source-raw.php b/tests/parser/sources/test-source-raw.php index 1fd4723b..f6d52b97 100644 --- a/tests/parser/sources/test-source-raw.php +++ b/tests/parser/sources/test-source-raw.php @@ -33,7 +33,7 @@ public function test_parse_raw_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -74,7 +74,7 @@ public function test_parse_raw_source__nested() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-rich-text.php b/tests/parser/sources/test-source-rich-text.php index 286bc691..26421cc3 100644 --- a/tests/parser/sources/test-source-rich-text.php +++ b/tests/parser/sources/test-source-rich-text.php @@ -36,7 +36,7 @@ public function test_parse_rich_text_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -69,7 +69,7 @@ public function test_parse_rich_text_source__with_formatting() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -100,7 +100,7 @@ public function test_parse_rich_text_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -131,7 +131,7 @@ public function test_parse_rich_text_source__with_default_value_with_formatting( ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-tag.php b/tests/parser/sources/test-source-tag.php index 93f4c4a9..51cfb2e1 100644 --- a/tests/parser/sources/test-source-tag.php +++ b/tests/parser/sources/test-source-tag.php @@ -35,7 +35,7 @@ public function test_parse_tag_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -79,7 +79,7 @@ public function test_parse_tag_source__in_query() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -106,7 +106,7 @@ public function test_parse_tag_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/sources/test-source-text.php b/tests/parser/sources/test-source-text.php index 6432d8eb..b63e12ce 100644 --- a/tests/parser/sources/test-source-text.php +++ b/tests/parser/sources/test-source-text.php @@ -38,7 +38,7 @@ public function test_parse_text_source() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -72,7 +72,7 @@ public function test_parse_text_source__with_html_tags() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -104,7 +104,7 @@ public function test_parse_text_source__with_default_value() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/test-block-bindings.php b/tests/parser/test-block-bindings.php new file mode 100644 index 00000000..757add11 --- /dev/null +++ b/tests/parser/test-block-bindings.php @@ -0,0 +1,295 @@ +markTestSkipped( 'This test suite requires WP_Block_Bindings_Registry (WordPress 6.5 or higher).' ); + } + } + + /* Single paragraph block binding */ + + public function test_single_paragraph_block_binding() { + + $this->register_block_bindings_source( + 'test/block-binding', + [ + 'label' => 'Test paragraph block binding', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'Block binding for %s with arg foo=%s', $block->name, $args['foo'] ); + }, + ] + ); + + $html = ' + +

Fallback content

+ + '; + + $expected_blocks = [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Block binding for core/paragraph with arg foo=bar', + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + } + + /* Image block with multiple bindings */ + + public function test_image_block_with_multiple_bindings() { + $this->register_block_bindings_source( + 'test/block-binding-image-url', + [ + 'label' => 'Test image block binding for URL', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'https://example.com/image.webp?block=%s&foo=%s', $block->name, $args['foo'] ); + }, + ] + ); + + $this->register_block_bindings_source( + 'test/block-binding-image-alt', + [ + 'label' => 'Test image block binding for alt text', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'Block binding for %s with arg foo=%s', $block->name, $args['foo'] ); + }, + ] + ); + + $html = ' + + Fallback alt text + + '; + + $expected_blocks = [ + [ + 'name' => 'core/image', + 'attributes' => [ + 'alt' => 'Block binding for core/image with arg foo=bar', + 'url' => 'https://example.com/image.webp?block=core/image&foo=bar', + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + } + + /* Paragraph block binding with default post context */ + + public function test_button_block_binding_with_default_context() { + $this->register_block_bindings_source( + 'test/block-binding-with-default-context', [ + 'label' => 'Test paragraph block binding with default context', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'Block binding for %s with arg foo=%s in %s %d', $block->name, $args['foo'], $block->context['postType'], $block->context['postId'] ); + }, + 'uses_context' => [ 'postId', 'postType' ], + ] + ); + + $html = ' + +

Fallback content

+ + '; + + $post = $this->factory()->post->create_and_get(); + + $expected_blocks = [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/block-binding-with-default-context', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html, $post->ID ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + } + + /* Nested paragraph block binding with context */ + + public function test_nested_paragraph_block_binding_with_custom_context() { + $this->register_block_bindings_source( + 'test/block-binding-with-custom-context', [ + 'label' => 'Test paragraph block binding with custom context', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'Block binding for %s with arg foo=%s and context fizz=%s', $block->name, $args['foo'], $block->context['my-context/fizz'] ?? 'missing' ); + }, + 'uses_context' => [ 'my-context/fizz' ], + ] + ); + + $this->register_block_with_attributes( + 'test/context-provider', + [ + 'fizz' => [ + 'type' => 'string', + ], + ], + [ + 'provides_context' => [ + 'my-context/fizz' => 'fizz', + ], + ] + ); + + $html = ' + + +

Fallback content

+ + + '; + + $expected_blocks = [ + [ + 'name' => 'test/context-provider', + 'attributes' => [ + 'fizz' => 'buzz', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Block binding for core/paragraph with arg foo=bar and context fizz=buzz', + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/block-binding-with-custom-context', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in result set' ); + } + + /* Missing block binding */ + + public function test_missing_block_binding() { + + $html = ' + +

Fallback content

+ + '; + + $expected_blocks = [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Fallback content', + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/missing-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 1, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + } +} diff --git a/tests/parser/test-content-parser.php b/tests/parser/test-content-parser.php index d11b65ec..9852b7c0 100644 --- a/tests/parser/test-content-parser.php +++ b/tests/parser/test-content-parser.php @@ -47,7 +47,7 @@ public function test_parse_multiple_attributes_from_block() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -98,7 +98,7 @@ public function test_parse_multiple_blocks() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -157,9 +157,62 @@ public function test_parse_block_missing_attributes_and_defaults() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); } + + /* Whitespace block removal */ + + public function test_parse_whitespace_block_removal() { + $this->register_block_with_attributes( 'test/block', [ + 'content' => [ + 'type' => 'string', + 'source' => 'html', + 'selector' => 'p', + ], + ] ); + + $html = join( [ + // Some intentional whitespace + ' + ', + ' +

Block 1

+ + ', + // Some intentional whitespace + ' + ', + ' +

Block 2

+ + ', + // Some intentional whitespace + ' + ', + ] ); + + $expected_blocks = [ + [ + 'name' => 'test/block', + 'attributes' => [ + 'content' => 'Block 1', + ], + ], + [ + 'name' => 'test/block', + 'attributes' => [ + 'content' => 'Block 2', + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'], sprintf( 'Blocks do not match: %s', wp_json_encode( $blocks ) ) ); + } } diff --git a/tests/parser/test-inner-blocks.php b/tests/parser/test-inner-blocks.php index 07f5d016..2348761c 100644 --- a/tests/parser/test-inner-blocks.php +++ b/tests/parser/test-inner-blocks.php @@ -88,7 +88,7 @@ public function test_parse_inner_blocks__one_layer() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); @@ -181,7 +181,7 @@ public function test_parse_inner_blocks__two_layers() { ], ]; - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); diff --git a/tests/parser/test-parser-filters.php b/tests/parser/test-parser-filters.php index de99543f..8b993e8e 100644 --- a/tests/parser/test-parser-filters.php +++ b/tests/parser/test-parser-filters.php @@ -59,7 +59,7 @@ public function test_allow_block_filter_via_code() { }; add_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); remove_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); @@ -100,7 +100,7 @@ public function test_before_parse_post_content_filter() { }; add_filter( 'vip_block_data_api__before_parse_post_content', $replace_post_content_filter ); - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $blocks = $content_parser->parse( $html ); remove_filter( 'vip_block_data_api__before_parse_post_content', $replace_post_content_filter ); @@ -143,7 +143,7 @@ public function test_after_parse_filter() { }; add_filter( 'vip_block_data_api__after_parse_blocks', $add_extra_data_filter ); - $content_parser = new ContentParser( $this->registry ); + $content_parser = new ContentParser( $this->get_block_registry() ); $result = $content_parser->parse( $html ); remove_filter( 'vip_block_data_api__after_parse_blocks', $add_extra_data_filter ); diff --git a/tests/parser/test-synced-patterns.php b/tests/parser/test-synced-patterns.php new file mode 100644 index 00000000..d08f9b07 --- /dev/null +++ b/tests/parser/test-synced-patterns.php @@ -0,0 +1,533 @@ +markTestSkipped( 'This test suite requires resolve_pattern_blocks (WordPress 6.6 or higher).' ); + } + } + + /* Simple synced pattern */ + + public function test_simple_synced_pattern() { + $this->register_block_with_attributes( 'test/custom-block', [ + 'content' => [ + 'type' => 'rich-text', + 'source' => 'rich-text', + 'selector' => 'p', + '__experimentalRole' => 'content', + ], + 'bing' => [ + 'type' => 'string', + 'source' => 'attribute', + 'selector' => 'p', + 'attribute' => 'data-bing', + ], + ] ); + + $synced_pattern_content = ' + +

My synced pattern content

+ + '; + + $synced_pattern = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $html = sprintf( '', $synced_pattern->ID ); + + $expected_blocks = [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My synced pattern content', + 'bing' => 'bong', + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'], sprintf( 'Blocks not equal: %s', wp_json_encode( $blocks['blocks'] ) ) ); + } + + /* Multiple synced patterns */ + + public function test_multiple_synced_patterns() { + $this->register_block_with_attributes( 'test/custom-block', [ + 'content' => [ + 'type' => 'rich-text', + 'source' => 'rich-text', + 'selector' => 'p', + '__experimentalRole' => 'content', + ], + 'bing' => [ + 'type' => 'string', + 'source' => 'attribute', + 'selector' => 'p', + 'attribute' => 'data-bing', + ], + ] ); + + $synced_pattern_content_1 = ' + +

My first synced pattern content

+ + '; + + $synced_pattern_1 = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content_1, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $synced_pattern_content_2 = ' + +

My second synced pattern content

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

Default content

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

My first synced pattern content

+ + + +

Fallback content

+ + + +

Default content

+ + '; + + $synced_pattern_1 = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content_1, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + $synced_pattern_content_2 = sprintf( ' + +

My second synced pattern content which contains the first

+ + + + + +

Another block to "wrap" the nested pattern

+ + ', $synced_pattern_1->ID ); + + $synced_pattern_2 = $this->factory()->post->create_and_get( [ + 'post_content' => $synced_pattern_content_2, + 'post_status' => 'publish', + 'post_type' => 'wp_block', + ] ); + + // This uses the default post context. Custom block binding context is not + // yet supported inside synced patterns. + $this->register_block_bindings_source( + 'test/synced-pattern-block-binding', + [ + 'label' => 'Block binding inside synced pattern', + 'get_value_callback' => static function ( array $args, WP_Block $block ) { + return sprintf( 'Block binding for %s with arg foo=%s in %s %d', $block->name, $args['foo'], $block->context['postType'] ?? 'unknown', $block->context['postId'] ?? 'unknown' ); + }, + 'uses_context' => [ 'postId', 'postType' ], + ] + ); + + $html = sprintf( ' + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + ', $synced_pattern_1->ID, $synced_pattern_1->ID, $synced_pattern_2->ID ); + + $post = $this->factory()->post->create_and_get(); + + $expected_blocks = [ + [ + 'name' => 'test/custom-container', + 'attributes' => [ + 'fizz' => 'buzz', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern_1->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Default content', + 'metadata' => [ + 'bindings' => [ + '__default' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', + ], + ], + ], + ], + ], + ], + ], + [ + 'name' => 'test/custom-container', + 'attributes' => [ + 'fizz' => 'buzz', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern_1->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Overridden content', // Overridden by synced pattern override + 'metadata' => [ + 'bindings' => [ + '__default' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', + ], + ], + ], + ], + ], + ], + ], + [ + 'name' => 'test/custom-container', + 'attributes' => [ + 'fizz' => 'bazz', + ], + 'innerBlocks' => [ + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern_2->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My second synced pattern content which contains the first', + 'bing' => 'bang', + ], + ], + [ + 'name' => 'core/block', + 'attributes' => [ + 'ref' => $synced_pattern_1->ID, + ], + 'innerBlocks' => [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'My first synced pattern content', + 'bing' => 'bong', + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => sprintf( 'Block binding for core/paragraph with arg foo=bar in post %d', $post->ID ), + 'metadata' => [ + 'bindings' => [ + 'content' => [ + 'source' => 'test/synced-pattern-block-binding', + 'args' => [ 'foo' => 'bar' ], + ], + ], + ], + ], + ], + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Default content', + 'metadata' => [ + 'bindings' => [ + '__default' => [ + 'source' => 'core/pattern-overrides', + ], + ], + 'name' => 'my-override', + ], + ], + ], + ], + ], + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'content' => 'Another block to "wrap" the nested pattern', + 'bing' => 'bang', + ], + ], + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->get_block_registry() ); + $blocks = $content_parser->parse( $html, $post->ID ); + + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + + // Block bindings are currently only supported for specific core blocks. + // https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/ + // + // Core block attributes can change, so we use assertArraySubset to avoid + // brittle assertions. + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], false, wp_json_encode( $blocks['blocks'] ) ); + $this->assertEquals( 3, count( $blocks['blocks'] ), 'Too many blocks in result set' ); + + // First synced pattern + $this->assertEquals( 1, count( $blocks['blocks'][0]['innerBlocks'] ), 'Too many inner blocks in first container block' ); + $this->assertEquals( 3, count( $blocks['blocks'][0]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + + // First synced pattern, repeated (contains pattern override) + $this->assertEquals( 1, count( $blocks['blocks'][1]['innerBlocks'] ), 'Too many inner blocks in first container block' ); + $this->assertEquals( 3, count( $blocks['blocks'][1]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in first synced pattern' ); + + // Second synced pattern + $this->assertEquals( 1, count( $blocks['blocks'][2]['innerBlocks'] ), 'Too many inner blocks in second container block' ); + $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'] ), 'Too many inner blocks in second synced pattern' ); + $this->assertEquals( 3, count( $blocks['blocks'][2]['innerBlocks'][0]['innerBlocks'][1]['innerBlocks'] ), 'Too many inner blocks in nested pattern in second synced pattern' ); + } +} diff --git a/tests/registry-test-case.php b/tests/registry-test-case.php index d2d4004c..7abfafbf 100644 --- a/tests/registry-test-case.php +++ b/tests/registry-test-case.php @@ -3,6 +3,7 @@ namespace WPCOMVIP\BlockDataApi; use WP_Block_Type_Registry; +use WP_Block_Bindings_Registry; use WP_UnitTestCase; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; @@ -12,18 +13,27 @@ class RegistryTestCase extends WP_UnitTestCase { use ArraySubsetAsserts; - protected $registry; - protected $globally_registered_blocks = []; - - protected function setUp(): void { - parent::setUp(); + protected function tearDown(): void { + // Unregister non-core blocks. + $block_registry = WP_Block_Type_Registry::get_instance(); + foreach ( $block_registry->get_all_registered() as $block_type ) { + if ( 'core/' === substr( $block_type->name, 0, 5 ) ) { + continue; + } + + $block_registry->unregister( $block_type->name ); + } - $this->registry = new WP_Block_Type_Registry(); - } + if ( class_exists( 'WP_Block_Bindings_Registry' ) ) { + // Unregister non-core block bindings. + $block_bindings_registry = WP_Block_Bindings_Registry::get_instance(); + foreach ( $block_bindings_registry->get_all_registered() as $source ) { + if ( 'core/' === substr( $source->name, 0, 5 ) ) { + continue; + } - protected function tearDown(): void { - foreach ( $this->globally_registered_blocks as $block_name ) { - $this->unregister_global_block( $block_name ); + $block_bindings_registry->unregister( $source->name ); + } } parent::tearDown(); @@ -31,31 +41,20 @@ protected function tearDown(): void { /* Helper methods */ - protected function register_block_with_attributes( $block_name, $attributes ) { - $this->registry->register( $block_name, [ - 'apiVersion' => 2, - 'attributes' => $attributes, - ] ); + protected function get_block_registry(): WP_Block_Type_Registry { + return WP_Block_Type_Registry::get_instance(); } - /* Global registrations */ - - protected function register_global_block_with_attributes( $block_name, $attributes ) { - // Use this function for mocking blocks definitions that need to persist across HTTP requests, like GraphQL tests. - - WP_Block_Type_Registry::get_instance()->register( $block_name, [ + protected function register_block_with_attributes( string $block_name, array $attributes, array $additional_args = [] ): void { + $block_type_args = array_merge( [ 'apiVersion' => 2, 'attributes' => $attributes, - ] ); + ], $additional_args ); - $this->globally_registered_blocks[] = $block_name; + $this->get_block_registry()->register( $block_name, $block_type_args ); } - protected function unregister_global_block( $block_name ) { - $registry = WP_Block_Type_Registry::get_instance(); - - if ( $registry->is_registered( $block_name ) ) { - $registry->unregister( $block_name ); - } + protected function register_block_bindings_source( string $source, array $args ): void { + WP_Block_Bindings_Registry::get_instance()->register( $source, $args ); } } diff --git a/tests/rest/test-rest-api.php b/tests/rest/test-rest-api.php index 0955bdb4..bc6b5386 100644 --- a/tests/rest/test-rest-api.php +++ b/tests/rest/test-rest-api.php @@ -8,17 +8,14 @@ namespace WPCOMVIP\BlockDataApi; use Exception; -use WP_Block_Type_Registry; -use WP_UnitTestCase; use WP_REST_Server; use WP_REST_Request; /** * e2e tests to ensure that the REST API endpoint is available. */ -class RestApiTest extends WP_UnitTestCase { +class RestApiTest extends RegistryTestCase { private $server; - private $globally_registered_blocks = []; protected function setUp(): void { parent::setUp(); @@ -34,15 +31,11 @@ protected function tearDown(): void { global $wp_rest_server; $wp_rest_server = null; - foreach ( $this->globally_registered_blocks as $block_name ) { - $this->unregister_global_block( $block_name ); - } - parent::tearDown(); } public function test_rest_api_returns_blocks_for_post() { - $this->register_global_block_with_attributes( 'test/custom-heading', [ + $this->register_block_with_attributes( 'test/custom-heading', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -55,7 +48,7 @@ public function test_rest_api_returns_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-quote', [ + $this->register_block_with_attributes( 'test/custom-quote', [ 'value' => [ 'type' => 'string', 'source' => 'html', @@ -72,7 +65,7 @@ public function test_rest_api_returns_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -88,14 +81,14 @@ public function test_rest_api_returns_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-separator', [ + $this->register_block_with_attributes( 'test/custom-separator', [ 'opacity' => [ 'type' => 'string', 'default' => 'alpha-channel', ], ] ); - $this->register_global_block_with_attributes( 'test/custom-media-text', [ + $this->register_block_with_attributes( 'test/custom-media-text', [ 'align' => [ 'type' => 'string', 'default' => 'none', @@ -245,7 +238,7 @@ public function test_rest_api_returns_blocks_for_post() { } public function test_rest_api_does_not_return_excluded_blocks_for_post() { - $this->register_global_block_with_attributes( 'test/custom-heading', [ + $this->register_block_with_attributes( 'test/custom-heading', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -258,7 +251,7 @@ public function test_rest_api_does_not_return_excluded_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-quote', [ + $this->register_block_with_attributes( 'test/custom-quote', [ 'value' => [ 'type' => 'string', 'source' => 'html', @@ -275,7 +268,7 @@ public function test_rest_api_does_not_return_excluded_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -291,14 +284,14 @@ public function test_rest_api_does_not_return_excluded_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-separator', [ + $this->register_block_with_attributes( 'test/custom-separator', [ 'opacity' => [ 'type' => 'string', 'default' => 'alpha-channel', ], ] ); - $this->register_global_block_with_attributes( 'test/custom-media-text', [ + $this->register_block_with_attributes( 'test/custom-media-text', [ 'align' => [ 'type' => 'string', 'default' => 'none', @@ -426,7 +419,7 @@ public function test_rest_api_does_not_return_excluded_blocks_for_post() { } public function test_rest_api_only_returns_included_blocks_for_post() { - $this->register_global_block_with_attributes( 'test/custom-heading', [ + $this->register_block_with_attributes( 'test/custom-heading', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -439,7 +432,7 @@ public function test_rest_api_only_returns_included_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-quote', [ + $this->register_block_with_attributes( 'test/custom-quote', [ 'value' => [ 'type' => 'string', 'source' => 'html', @@ -456,7 +449,7 @@ public function test_rest_api_only_returns_included_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -472,14 +465,14 @@ public function test_rest_api_only_returns_included_blocks_for_post() { ], ] ); - $this->register_global_block_with_attributes( 'test/custom-separator', [ + $this->register_block_with_attributes( 'test/custom-separator', [ 'opacity' => [ 'type' => 'string', 'default' => 'alpha-channel', ], ] ); - $this->register_global_block_with_attributes( 'test/custom-media-text', [ + $this->register_block_with_attributes( 'test/custom-media-text', [ 'align' => [ 'type' => 'string', 'default' => 'none', @@ -590,7 +583,7 @@ public function test_rest_api_returns_blocks_for_custom_post_type() { 'show_in_rest' => true, ]); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -645,7 +638,7 @@ public function test_rest_api_returns_error_for_non_public_post_type() { 'public' => false, ]); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -681,7 +674,7 @@ public function test_rest_api_returns_error_for_non_rest_post_type() { 'show_in_rest' => false, ]); - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -712,7 +705,7 @@ public function test_rest_api_returns_error_for_non_rest_post_type() { } public function test_rest_api_returns_error_for_unpublished_post() { - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -763,7 +756,7 @@ public function test_rest_api_returns_error_for_classic_content() { } public function test_rest_api_returns_error_for_include_and_exclude_filter() { - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -798,7 +791,7 @@ public function test_rest_api_returns_error_for_include_and_exclude_filter() { } public function test_rest_api_returns_error_for_unexpected_exception() { - $this->register_global_block_with_attributes( 'test/custom-paragraph', [ + $this->register_block_with_attributes( 'test/custom-paragraph', [ 'content' => [ 'type' => 'rich-text', 'source' => 'rich-text', @@ -857,21 +850,4 @@ static function ( int $errno, string $errstr ): never { E_USER_WARNING ); } - - private function register_global_block_with_attributes( $block_name, $attributes ) { - WP_Block_Type_Registry::get_instance()->register( $block_name, [ - 'apiVersion' => 2, - 'attributes' => $attributes, - ] ); - - $this->globally_registered_blocks[] = $block_name; - } - - private function unregister_global_block( $block_name ) { - $registry = WP_Block_Type_Registry::get_instance(); - - if ( $registry->is_registered( $block_name ) ) { - $registry->unregister( $block_name ); - } - } } diff --git a/vip-block-data-api.php b/vip-block-data-api.php index 52efe4f2..0ddb80d0 100644 --- a/vip-block-data-api.php +++ b/vip-block-data-api.php @@ -5,7 +5,7 @@ * Description: Access Gutenberg block data in JSON via the REST API. * Author: WordPress VIP * Text Domain: vip-block-data-api - * Version: 1.3.0 + * Version: 1.4.0 * Requires at least: 6.0 * Tested up to: 6.6 * Requires PHP: 8.0 @@ -20,7 +20,7 @@ if ( ! defined( 'VIP_BLOCK_DATA_API_LOADED' ) ) { define( 'VIP_BLOCK_DATA_API_LOADED', true ); - define( 'WPCOMVIP__BLOCK_DATA_API__PLUGIN_VERSION', '1.3.0' ); + define( 'WPCOMVIP__BLOCK_DATA_API__PLUGIN_VERSION', '1.4.0' ); define( 'WPCOMVIP__BLOCK_DATA_API__REST_ROUTE', 'vip-block-data-api/v1' ); // Analytics related configs. @@ -40,6 +40,7 @@ // Block parsing. require_once __DIR__ . '/src/parser/content-parser.php'; + require_once __DIR__ . '/src/parser/block-additions/core-block.php'; require_once __DIR__ . '/src/parser/block-additions/core-image.php'; // Analytics.