From 910de8f6890c87f93359c6f2edc6c27b9a3f3292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 30 Jun 2020 11:02:22 +0000 Subject: [PATCH] Editor: Introduce block context Backports a new block context feature from Gutenberg. The purpose of this feature is to be able to establish values in a block hierarchy which can be consumed by blocks anywhere lower in the same hierarchy. These values can be established either by the framework, or by other blocks which provide these values. See documentation: https://github.com/WordPress/gutenberg/blob/master/docs/designers-developers/developers/block-api/block-context.md Props aduth, epiqueras. Fixes #49927. git-svn-id: https://develop.svn.wordpress.org/trunk@48224 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/blocks.php | 67 +++--- src/wp-includes/class-wp-block.php | 9 +- tests/phpunit/tests/blocks/block-context.php | 215 +++++++++++++++++++ 3 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 tests/phpunit/tests/blocks/block-context.php diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 9b612d68bd..4881ab4d87 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -649,67 +649,74 @@ function _excerpt_render_inner_columns_blocks( $columns, $allowed_blocks ) { * * @since 5.0.0 * - * @global WP_Post $post The post to edit. + * @global WP_Post $post The post to edit. + * @global WP_Query $wp_the_query WordPress Query object. * - * @param array $block A single parsed block object. + * @param array $parsed_block A single parsed block object. * @return string String of rendered HTML. */ -function render_block( $block ) { - global $post; +function render_block( $parsed_block ) { + global $post, $wp_query; /** * Allows render_block() to be short-circuited, by returning a non-null value. * * @since 5.1.0 * - * @param string|null $pre_render The pre-rendered content. Default null. - * @param array $block The block being rendered. + * @param string|null $pre_render The pre-rendered content. Default null. + * @param array $parsed_block The block being rendered. */ - $pre_render = apply_filters( 'pre_render_block', null, $block ); + $pre_render = apply_filters( 'pre_render_block', null, $parsed_block ); if ( ! is_null( $pre_render ) ) { return $pre_render; } - $source_block = $block; + $source_block = $parsed_block; /** * Filters the block being rendered in render_block(), before it's processed. * * @since 5.1.0 * - * @param array $block The block being rendered. - * @param array $source_block An un-modified copy of $block, as it appeared in the source content. + * @param array $parsed_block The block being rendered. + * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. */ - $block = apply_filters( 'render_block_data', $block, $source_block ); + $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block ); - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $is_dynamic = $block['blockName'] && null !== $block_type && $block_type->is_dynamic(); - $block_content = ''; - $index = 0; + $context = array(); - foreach ( $block['innerContent'] as $chunk ) { - $block_content .= is_string( $chunk ) ? $chunk : render_block( $block['innerBlocks'][ $index++ ] ); + if ( ! empty( $post ) ) { + $context['postId'] = $post->ID; + + /* + * The `postType` context is largely unnecessary server-side, since the + * ID is usually sufficient on its own. That being said, since a block's + * manifest is expected to be shared between the server and the client, + * it should be included to consistently fulfill the expectation. + */ + $context['postType'] = $post->post_type; } - if ( ! is_array( $block['attrs'] ) ) { - $block['attrs'] = array(); - } - - if ( $is_dynamic ) { - $global_post = $post; - $block_content = $block_type->render( $block['attrs'], $block_content ); - $post = $global_post; + if ( isset( $wp_query->tax_query->queried_terms['category'] ) ) { + $context['query'] = array( 'categoryIds' => array() ); + foreach ( $wp_query->tax_query->queried_terms['category']['terms'] as $category_slug_or_id ) { + $context['query']['categoryIds'][] = 'slug' === $wp_query->tax_query->queried_terms['category']['field'] ? get_cat_ID( $category_slug_or_id ) : $category_slug_or_id; + } } /** - * Filters the content of a single block. + * Filters the default context provided to a rendered block. * - * @since 5.0.0 + * @since 5.5.0 * - * @param string $block_content The block content about to be appended. - * @param array $block The full block, including name and attributes. + * @param array $context Default context. + * @param array $parsed_block Block being rendered, filtered by `render_block_data`. */ - return apply_filters( 'render_block', $block_content, $block ); + $context = apply_filters( 'render_block_context', $context, $parsed_block ); + + $block = new WP_Block( $parsed_block, $context ); + + return $block->render(); } /** diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 979fca9070..325ca6e65c 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -226,7 +226,14 @@ class WP_Block { wp_enqueue_style( $this->block_type->style ); } - /** This filter is documented in src/wp-includes/blocks.php */ + /** + * Filters the content of a single block. + * + * @since 5.0.0 + * + * @param string $block_content The block content about to be appended. + * @param array $block The full block, including name and attributes. + */ return apply_filters( 'render_block', $block_content, $this->parsed_block ); } diff --git a/tests/phpunit/tests/blocks/block-context.php b/tests/phpunit/tests/blocks/block-context.php new file mode 100644 index 0000000000..61baf3b611 --- /dev/null +++ b/tests/phpunit/tests/blocks/block-context.php @@ -0,0 +1,215 @@ + 'example', + 'post_excerpt' => '', + ); + + $post = $this->factory()->post->create_and_get( $args ); + setup_postdata( $post ); + } + + /** + * Tear down each test method. + */ + public function tearDown() { + parent::tearDown(); + + while ( ! empty( $this->registered_block_names ) ) { + $block_name = array_pop( $this->registered_block_names ); + unregister_block_type( $block_name ); + } + } + + /** + * Registers a block type. + * + * @param string|WP_Block_Type $name Block type name including namespace, or alternatively a + * complete WP_Block_Type instance. In case a WP_Block_Type + * is provided, the $args parameter will be ignored. + * @param array $args { + * Optional. Array of block type arguments. Any arguments may be defined, however the + * ones described below are supported by default. Default empty array. + * + * @type callable $render_callback Callback used to render blocks of this block type. + * } + */ + protected function register_block_type( $name, $args ) { + register_block_type( $name, $args ); + + $this->registered_block_names[] = $name; + } + + /** + * Tests that a block which provides context makes that context available to + * its inner blocks. + * + * @ticket 49927 + */ + function test_provides_block_context() { + $provided_context = array(); + + $this->register_block_type( + 'gutenberg/test-context-provider', + array( + 'attributes' => array( + 'contextWithAssigned' => array( + 'type' => 'number', + ), + 'contextWithDefault' => array( + 'type' => 'number', + 'default' => 0, + ), + 'contextWithoutDefault' => array( + 'type' => 'number', + ), + 'contextNotRequested' => array( + 'type' => 'number', + ), + ), + 'provides_context' => array( + 'gutenberg/contextWithAssigned' => 'contextWithAssigned', + 'gutenberg/contextWithDefault' => 'contextWithDefault', + 'gutenberg/contextWithoutDefault' => 'contextWithoutDefault', + 'gutenberg/contextNotRequested' => 'contextNotRequested', + ), + ) + ); + + $this->register_block_type( + 'gutenberg/test-context-consumer', + array( + 'uses_context' => array( + 'gutenberg/contextWithDefault', + 'gutenberg/contextWithAssigned', + 'gutenberg/contextWithoutDefault', + ), + 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $parsed_blocks = parse_blocks( + '' . + '' . + '' + ); + + render_block( $parsed_blocks[0] ); + + $this->assertEquals( + array( + 'gutenberg/contextWithDefault' => 0, + 'gutenberg/contextWithAssigned' => 10, + ), + $provided_context[0] + ); + } + + /** + * Tests that a block can receive default-provided context through + * render_block. + * + * @ticket 49927 + */ + function test_provides_default_context() { + global $post; + + $provided_context = array(); + + $this->register_block_type( + 'gutenberg/test-context-consumer', + array( + 'uses_context' => array( 'postId', 'postType' ), + 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $parsed_blocks = parse_blocks( '' ); + + render_block( $parsed_blocks[0] ); + + $this->assertEquals( + array( + 'postId' => $post->ID, + 'postType' => $post->post_type, + ), + $provided_context[0] + ); + } + + /** + * Tests that default block context can be filtered. + * + * @ticket 49927 + */ + function test_default_context_is_filterable() { + $provided_context = array(); + + $this->register_block_type( + 'gutenberg/test-context-consumer', + array( + 'uses_context' => array( 'example' ), + 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $filter_block_context = function( $context ) { + $context['example'] = 'ok'; + return $context; + }; + + $parsed_blocks = parse_blocks( '' ); + + add_filter( 'render_block_context', $filter_block_context ); + + render_block( $parsed_blocks[0] ); + + remove_filter( 'render_block_context', $filter_block_context ); + + $this->assertEquals( array( 'example' => 'ok' ), $provided_context[0] ); + } + +}