diff --git a/src/wp-includes/class-wp-block-list.php b/src/wp-includes/class-wp-block-list.php new file mode 100644 index 0000000000000..378faba578b11 --- /dev/null +++ b/src/wp-includes/class-wp-block-list.php @@ -0,0 +1,203 @@ +blocks = $blocks; + $this->available_context = $available_context; + $this->registry = $registry; + } + + /** + * Returns true if a block exists by the specified block index, or false + * otherwise. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php + * + * @param string $index Index of block to check. + * @return bool Whether block exists. + */ + public function offsetExists( $index ) { + return isset( $this->blocks[ $index ] ); + } + + /** + * Returns the value by the specified block index. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/arrayaccess.offsetget.php + * + * @param string $index Index of block value to retrieve. + * @return mixed|null Block value if exists, or null. + */ + public function offsetGet( $index ) { + $block = $this->blocks[ $index ]; + + if ( isset( $block ) && is_array( $block ) ) { + $block = new WP_Block( $block, $this->available_context, $this->registry ); + $this->blocks[ $index ] = $block; + } + + return $block; + } + + /** + * Assign a block value by the specified block index. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/arrayaccess.offsetset.php + * + * @param string $index Index of block value to set. + * @param mixed $value Block value. + */ + public function offsetSet( $index, $value ) { + if ( is_null( $index ) ) { + $this->blocks[] = $value; + } else { + $this->blocks[ $index ] = $value; + } + } + + /** + * Unset a block. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php + * + * @param string $index Index of block value to unset. + */ + public function offsetUnset( $index ) { + unset( $this->blocks[ $index ] ); + } + + /** + * Rewinds back to the first element of the Iterator. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/iterator.rewind.php + */ + public function rewind() { + reset( $this->blocks ); + } + + /** + * Returns the current element of the block list. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/iterator.current.php + * + * @return mixed Current element. + */ + public function current() { + return $this->offsetGet( $this->key() ); + } + + /** + * Returns the key of the current element of the block list. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/iterator.key.php + * + * @return mixed Key of the current element. + */ + public function key() { + return key( $this->blocks ); + } + + /** + * Moves the current position of the block list to the next element. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/iterator.next.php + */ + public function next() { + next( $this->blocks ); + } + + /** + * Checks if current position is valid. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/iterator.valid.php + */ + public function valid() { + return null !== key( $this->blocks ); + } + + /** + * Returns the count of blocks in the list. + * + * @since 5.5.0 + * + * @link https://www.php.net/manual/en/countable.count.php + * + * @return int Block count. + */ + public function count() { + return count( $this->blocks ); + } + +} diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php new file mode 100644 index 0000000000000..979fca90708f1 --- /dev/null +++ b/src/wp-includes/class-wp-block.php @@ -0,0 +1,233 @@ + testing..." -> "Just testing..." + * + * @since 5.5.0 + * @var string + */ + public $inner_html = ''; + + /** + * List of string fragments and null markers where inner blocks were found + * + * @example array( + * 'inner_html' => 'BeforeInnerAfter', + * 'inner_blocks' => array( block, block ), + * 'inner_content' => array( 'Before', null, 'Inner', null, 'After' ), + * ) + * + * @since 5.5.0 + * @var array + */ + public $inner_content = array(); + + /** + * Constructor. + * + * Populates object properties from the provided block instance argument. + * + * The given array of context values will not necessarily be available on + * the instance itself, but is treated as the full set of values provided by + * the block's ancestry. This is assigned to the private `available_context` + * property. Only values which are configured to consumed by the block via + * its registered type will be assigned to the block's `context` property. + * + * @since 5.5.0 + * + * @param array $block Array of parsed block properties. + * @param array $available_context Optional array of ancestry context values. + * @param WP_Block_Type_Registry $registry Optional block type registry. + */ + public function __construct( $block, $available_context = array(), $registry = null ) { + $this->parsed_block = $block; + $this->name = $block['blockName']; + + if ( is_null( $registry ) ) { + $registry = WP_Block_Type_Registry::get_instance(); + } + + $this->block_type = $registry->get_registered( $this->name ); + + $this->available_context = $available_context; + + if ( ! empty( $this->block_type->uses_context ) ) { + foreach ( $this->block_type->uses_context as $context_name ) { + if ( array_key_exists( $context_name, $this->available_context ) ) { + $this->context[ $context_name ] = $this->available_context[ $context_name ]; + } + } + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $child_context = $this->available_context; + + if ( ! empty( $this->block_type->provides_context ) ) { + foreach ( $this->block_type->provides_context as $context_name => $attribute_name ) { + if ( array_key_exists( $attribute_name, $this->attributes ) ) { + $child_context[ $context_name ] = $this->attributes[ $attribute_name ]; + } + } + } + + $this->inner_blocks = new WP_Block_List( $block['innerBlocks'], $child_context, $registry ); + } + + if ( ! empty( $block['innerHTML'] ) ) { + $this->inner_html = $block['innerHTML']; + } + + if ( ! empty( $block['innerContent'] ) ) { + $this->inner_content = $block['innerContent']; + } + } + + /** + * Returns a value from an inaccessible property. + * + * This is used to lazily initialize the `attributes` property of a block, + * such that it is only prepared with default attributes at the time that + * the property is accessed. For all other inaccessible properties, a `null` + * value is returned. + * + * @since 5.5.0 + * + * @param string $name Property name. + * @return array|null Prepared attributes, or null. + */ + public function __get( $name ) { + if ( 'attributes' === $name ) { + $this->attributes = isset( $this->parsed_block['attrs'] ) ? + $this->parsed_block['attrs'] : + array(); + + if ( ! is_null( $this->block_type ) ) { + $this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes ); + } + + return $this->attributes; + } + + return null; + } + + /** + * Generates the render output for the block. + * + * @since 5.5.0 + * + * @param array $options { + * Optional options object. + * + * @type bool $dynamic Defaults to 'true'. Optionally set to false to avoid using the block's render_callback. + * } + * @return string Rendered block output. + */ + public function render( $options = array() ) { + global $post; + $options = wp_parse_args( + $options, + array( + 'dynamic' => true, + ) + ); + + $is_dynamic = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic(); + $block_content = ''; + + if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) { + $index = 0; + foreach ( $this->inner_content as $chunk ) { + $block_content .= is_string( $chunk ) ? + $chunk : + $this->inner_blocks[ $index++ ]->render(); + } + } + + if ( $is_dynamic ) { + $global_post = $post; + $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this ); + $post = $global_post; + } + + if ( ! empty( $this->block_type->script ) ) { + wp_enqueue_script( $this->block_type->script ); + } + + if ( ! empty( $this->block_type->style ) ) { + wp_enqueue_style( $this->block_type->style ); + } + + /** This filter is documented in src/wp-includes/blocks.php */ + return apply_filters( 'render_block', $block_content, $this->parsed_block ); + } + +} diff --git a/src/wp-settings.php b/src/wp-settings.php index a78be1c0918c0..a1cec18559a21 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -276,6 +276,8 @@ require ABSPATH . WPINC . '/class-wp-block-type.php'; require ABSPATH . WPINC . '/class-wp-block-styles-registry.php'; require ABSPATH . WPINC . '/class-wp-block-type-registry.php'; +require ABSPATH . WPINC . '/class-wp-block.php'; +require ABSPATH . WPINC . '/class-wp-block-list.php'; require ABSPATH . WPINC . '/class-wp-block-parser.php'; require ABSPATH . WPINC . '/blocks.php'; require ABSPATH . WPINC . '/blocks/archives.php'; diff --git a/tests/phpunit/tests/blocks/block-list.php b/tests/phpunit/tests/blocks/block-list.php new file mode 100644 index 0000000000000..c5d5e543acdea --- /dev/null +++ b/tests/phpunit/tests/blocks/block-list.php @@ -0,0 +1,112 @@ +registry = new WP_Block_Type_Registry(); + $this->registry->register( 'core/example', array() ); + } + + /** + * Tear down each test method. + */ + public function tearDown() { + parent::tearDown(); + + $this->registry = null; + } + + /** + * @ticket 49927 + */ + function test_array_access() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + + // Test "offsetExists". + $this->assertTrue( isset( $blocks[0] ) ); + + // Test "offsetGet". + $this->assertEquals( 'core/example', $blocks[0]->name ); + + // Test "offsetSet". + $parsed_blocks[0]['blockName'] = 'core/updated'; + $blocks[0] = new WP_Block( $parsed_blocks[0], $context, $this->registry ); + $this->assertEquals( 'core/updated', $blocks[0]->name ); + + // Test "offsetUnset". + unset( $blocks[0] ); + $this->assertFalse( isset( $blocks[0] ) ); + } + + /** + * @ticket 49927 + */ + function test_iterable() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + $assertions = 0; + + foreach ( $blocks as $block ) { + $this->assertEquals( 'core/example', $block->name ); + $assertions++; + foreach ( $block->inner_blocks as $inner_block ) { + $this->assertEquals( 'core/example', $inner_block->name ); + $assertions++; + } + } + + $blocks->rewind(); + while ( $blocks->valid() ) { + $key = $blocks->key(); + $block = $blocks->current(); + $this->assertEquals( 0, $key ); + $assertions++; + $this->assertEquals( 'core/example', $block->name ); + $assertions++; + $blocks->next(); + } + + $this->assertEquals( 4, $assertions ); + } + + /** + * @ticket 49927 + */ + function test_countable() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + + $this->assertEquals( 1, count( $blocks ) ); + } + +} diff --git a/tests/phpunit/tests/blocks/block.php b/tests/phpunit/tests/blocks/block.php new file mode 100644 index 0000000000000..2a702a5bc43c3 --- /dev/null +++ b/tests/phpunit/tests/blocks/block.php @@ -0,0 +1,397 @@ +registry = new WP_Block_Type_Registry(); + } + + /** + * Tear down each test method. + */ + public function tearDown() { + parent::tearDown(); + + $this->registry = null; + } + + function filter_render_block( $content, $parsed_block ) { + return 'Original: "' . $content . '", from block "' . $parsed_block['blockName'] . '"'; + } + + /** + * @ticket 49927 + */ + function test_constructor_assigns_properties_from_parsed_block() { + $this->registry->register( 'core/example', array() ); + + $parsed_blocks = parse_blocks( 'ab' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertSame( $parsed_block, $block->parsed_block ); + $this->assertEquals( $parsed_block['blockName'], $block->name ); + $this->assertEquals( $parsed_block['attrs'], $block->attributes ); + $this->assertEquals( $parsed_block['innerContent'], $block->inner_content ); + $this->assertEquals( $parsed_block['innerHTML'], $block->inner_html ); + } + + /** + * @ticket 49927 + */ + function test_constructor_assigns_block_type_from_registry() { + $block_type_settings = array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + ); + $this->registry->register( 'core/example', $block_type_settings ); + + $parsed_block = array( 'blockName' => 'core/example' ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertInstanceOf( WP_Block_Type::class, $block->block_type ); + $this->assertEquals( + $block_type_settings['attributes'], + $block->block_type->attributes + ); + } + + /** + * @ticket 49927 + */ + function test_lazily_assigns_attributes_with_defaults() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + ) + ); + + $parsed_block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'explicit' => 20, + ), + ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( + array( + 'defaulted' => 10, + 'explicit' => 20, + ), + $block->attributes + ); + } + + /** + * @ticket 49927 + */ + function test_lazily_assigns_attributes_with_only_defaults() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + ) + ); + + $parsed_block = array( + 'blockName' => 'core/example', + 'attrs' => array(), + ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); + // Intentionally call a second time, to ensure property was assigned. + $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); + } + + /** + * @ticket 49927 + */ + function test_constructor_assigns_context_from_block_type() { + $this->registry->register( + 'core/example', + array( + 'uses_context' => array( 'requested' ), + ) + ); + + $parsed_block = array( 'blockName' => 'core/example' ); + $context = array( + 'requested' => 'included', + 'unrequested' => 'not included', + ); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( array( 'requested' => 'included' ), $block->context ); + } + + /** + * @ticket 49927 + */ + function test_constructor_maps_inner_blocks() { + $this->registry->register( 'core/example', array() ); + + $parsed_blocks = parse_blocks( 'ab' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertCount( 1, $block->inner_blocks ); + $this->assertInstanceOf( WP_Block::class, $block->inner_blocks[0] ); + $this->assertEquals( 'core/example', $block->inner_blocks[0]->name ); + } + + /** + * @ticket 49927 + */ + function test_constructor_prepares_context_for_inner_blocks() { + $this->registry->register( + 'core/outer', + array( + 'attributes' => array( + 'recordId' => array( + 'type' => 'number', + ), + ), + 'provides_context' => array( + 'core/recordId' => 'recordId', + ), + ) + ); + $this->registry->register( + 'core/inner', + array( + 'uses_context' => array( 'core/recordId' ), + ) + ); + + $parsed_blocks = parse_blocks( '' ); + $parsed_block = $parsed_blocks[0]; + $context = array( 'unrequested' => 'not included' ); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertCount( 0, $block->context ); + $this->assertEquals( + array( 'core/recordId' => 10 ), + $block->inner_blocks[0]->context + ); + } + + /** + * @ticket 49927 + */ + function test_constructor_assigns_merged_context() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'value' => array( + 'type' => array( 'string', 'null' ), + ), + ), + 'provides_context' => array( + 'core/value' => 'value', + ), + 'uses_context' => array( 'core/value' ), + ) + ); + + $parsed_blocks = parse_blocks( + '' . + '' . + '' . + '' . + '' + ); + $parsed_block = $parsed_blocks[0]; + $context = array( 'core/value' => 'original' ); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( + array( 'core/value' => 'original' ), + $block->context + ); + $this->assertEquals( + array( 'core/value' => 'merged' ), + $block->inner_blocks[0]->context + ); + $this->assertEquals( + array( 'core/value' => null ), + $block->inner_blocks[0]->inner_blocks[0]->context + ); + } + + /** + * @ticket 49927 + */ + function test_render_static_block_type_returns_own_content() { + $this->registry->register( 'core/static', array() ); + $this->registry->register( + 'core/dynamic', + array( + 'render_callback' => function() { + return 'b'; + }, + ) + ); + + $parsed_blocks = parse_blocks( 'ac' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertSame( 'abc', $block->render() ); + } + + /** + * @ticket 49927 + */ + function test_render_passes_block_for_render_callback() { + $this->registry->register( + 'core/greeting', + array( + 'render_callback' => function( $attributes, $content, $block ) { + return sprintf( 'Hello from %s', $block->name ); + }, + ) + ); + + $parsed_blocks = parse_blocks( '' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertSame( 'Hello from core/greeting', $block->render() ); + } + + /** + * @ticket 49927 + */ + function test_render_applies_render_block_filter() { + $this->registry->register( 'core/example', array() ); + + add_filter( 'render_block', array( $this, 'filter_render_block' ), 10, 2 ); + + $parsed_blocks = parse_blocks( 'StaticInner' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $rendered_content = $block->render(); + + remove_filter( 'render_block', array( $this, 'filter_render_block' ) ); + + $this->assertSame( 'Original: "StaticOriginal: "Inner", from block "core/example"", from block "core/example"', $rendered_content ); + + } + + /** + * @ticket 49927 + */ + function test_passes_attributes_to_render_callback() { + $this->registry->register( + 'core/greeting', + array( + 'attributes' => array( + 'toWhom' => array( + 'type' => 'string', + ), + 'punctuation' => array( + 'type' => 'string', + 'default' => '!', + ), + ), + 'render_callback' => function( $block_attributes ) { + return sprintf( + 'Hello %s%s', + $block_attributes['toWhom'], + $block_attributes['punctuation'] + ); + }, + ) + ); + + $parsed_blocks = parse_blocks( '' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertSame( 'Hello world!', $block->render() ); + } + + /** + * @ticket 49927 + */ + function test_passes_content_to_render_callback() { + $this->registry->register( + 'core/outer', + array( + 'render_callback' => function( $block_attributes, $content ) { + return $content; + }, + ) + ); + $this->registry->register( + 'core/inner', + array( + 'render_callback' => function() { + return 'b'; + }, + ) + ); + + $parsed_blocks = parse_blocks( 'ac' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertSame( 'abc', $block->render() ); + } + +}