From 5f6ab44340e7ecd4e99bbf5841e2f0689c4f8d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 23 Jun 2020 15:43:19 +0000 Subject: [PATCH] Editor: Introduce new API method that register block from `block.json` metadata file Backports changes added to Gutenberg in: - https://github.com/WordPress/gutenberg/pull/20794 - https://github.com/WordPress/gutenberg/pull/22519 `register_block_type_from_metadata` function is going to be used to register all blocks on the server using `block.json` metadata files. Props ocean90, azaozz, aduth, mcsf, jorgefilipecosta, spacedmonkey, nosolosw, swissspidy and noahtallen. Fixes #50263. git-svn-id: https://develop.svn.wordpress.org/trunk@48141 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/blocks.php | 211 +++++++++++++++ .../tests/blocks/fixtures/block.asset.php | 6 + tests/phpunit/tests/blocks/fixtures/block.css | 1 + tests/phpunit/tests/blocks/fixtures/block.js | 1 + .../phpunit/tests/blocks/fixtures/block.json | 52 ++++ tests/phpunit/tests/blocks/register.php | 242 ++++++++++++++++++ 6 files changed, 513 insertions(+) create mode 100644 tests/phpunit/tests/blocks/fixtures/block.asset.php create mode 100644 tests/phpunit/tests/blocks/fixtures/block.css create mode 100644 tests/phpunit/tests/blocks/fixtures/block.js create mode 100644 tests/phpunit/tests/blocks/fixtures/block.json diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 3769b936cdee9..0a34af25f6168 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -40,6 +40,217 @@ function unregister_block_type( $name ) { return WP_Block_Type_Registry::get_instance()->unregister( $name ); } +/** + * Removes the block asset's path prefix if provided. + * + * @since 5.5.0 + * + * @param string $asset_handle_or_path Asset handle or prefixed path. + * @return string Path without the prefix or the original value. + */ +function remove_block_asset_path_prefix( $asset_handle_or_path ) { + $path_prefix = 'file:'; + if ( 0 !== strpos( $asset_handle_or_path, $path_prefix ) ) { + return $asset_handle_or_path; + } + return substr( + $asset_handle_or_path, + strlen( $path_prefix ) + ); +} + +/** + * Generates the name for an asset based on the name of the block + * and the field name provided. + * + * @since 5.5.0 + * + * @param string $block_name Name of the block. + * @param string $field_name Name of the metadata field. + * @return string Generated asset name for the block's field. + */ +function generate_block_asset_handle( $block_name, $field_name ) { + $field_mappings = array( + 'editorScript' => 'editor-script', + 'script' => 'script', + 'editorStyle' => 'editor-style', + 'style' => 'style', + ); + return str_replace( '/', '-', $block_name ) . + '-' . $field_mappings[ $field_name ]; +} + +/** + * Finds a script handle for the selected block metadata field. It detects + * when a path to file was provided and finds a corresponding + * asset file with details necessary to register the script under + * automatically generated handle name. It returns unprocessed script handle + * otherwise. + * + * @since 5.5.0 + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @return string|bool Script handle provided directly or created through + * script's registration, or false on failure. + */ +function register_block_script_handle( $metadata, $field_name ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + $script_handle = $metadata[ $field_name ]; + $script_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); + if ( $script_handle === $script_path ) { + return $script_handle; + } + + $script_handle = generate_block_asset_handle( $metadata['name'], $field_name ); + $script_asset_path = realpath( + dirname( $metadata['file'] ) . '/' . + substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ) + ); + if ( ! file_exists( $script_asset_path ) ) { + $message = sprintf( + /* translators: %1: field name. %2: block name */ + __( 'The asset file for the "%1$s" defined in "%2$s" block definition is missing.', 'default' ), + $field_name, + $metadata['name'] + ); + _doing_it_wrong( __FUNCTION__, $message, '5.5.0' ); + return false; + } + $script_asset = require $script_asset_path; + $result = wp_register_script( + $script_handle, + plugins_url( $script_path, $metadata['file'] ), + $script_asset['dependencies'], + $script_asset['version'] + ); + return $result ? $script_handle : false; +} + +/** + * Finds a style handle for the block metadata field. It detects when a path + * to file was provided and registers the style under automatically + * generated handle name. It returns unprocessed style handle otherwise. + * + * @since 5.5.0 + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @return string|boolean Style handle provided directly or created through + * style's registration, or false on failure. + */ +function register_block_style_handle( $metadata, $field_name ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + $style_handle = $metadata[ $field_name ]; + $style_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); + if ( $style_handle === $style_path ) { + return $style_handle; + } + + $style_handle = generate_block_asset_handle( $metadata['name'], $field_name ); + $block_dir = dirname( $metadata['file'] ); + $result = wp_register_style( + $style_handle, + plugins_url( $style_path, $metadata['file'] ), + array(), + filemtime( realpath( "$block_dir/$style_path" ) ) + ); + return $result ? $style_handle : false; +} + +/** + * Registers a block type from metadata stored in the `block.json` file. + * + * @since 5.5.0 + * + * @param string $file_or_folder Path to the JSON file with metadata definition for + * the block or path to the folder where the `block.json` file is located. + * @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. + * } + * @return WP_Block_Type|false The registered block type on success, or false on failure. + */ +function register_block_type_from_metadata( $file_or_folder, $args = array() ) { + $filename = 'block.json'; + $metadata_file = ( substr( $file_or_folder, -strlen( $filename ) ) !== $filename ) ? + trailingslashit( $file_or_folder ) . $filename : + $file_or_folder; + if ( ! file_exists( $metadata_file ) ) { + return false; + } + + $metadata = json_decode( file_get_contents( $metadata_file ), true ); + if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) { + return false; + } + $metadata['file'] = $metadata_file; + + $settings = array(); + $property_mappings = array( + 'title' => 'title', + 'category' => 'category', + 'parent' => 'parent', + 'icon' => 'icon', + 'description' => 'description', + 'keywords' => 'keywords', + 'attributes' => 'attributes', + 'providesContext' => 'provides_context', + 'usesContext' => 'uses_context', + 'supports' => 'supports', + 'styles' => 'styles', + 'example' => 'example', + ); + + foreach ( $property_mappings as $key => $mapped_key ) { + if ( isset( $metadata[ $key ] ) ) { + $settings[ $mapped_key ] = $metadata[ $key ]; + } + } + + if ( ! empty( $metadata['editorScript'] ) ) { + $settings['editor_script'] = register_block_script_handle( + $metadata, + 'editorScript' + ); + } + + if ( ! empty( $metadata['script'] ) ) { + $settings['script'] = register_block_script_handle( + $metadata, + 'script' + ); + } + + if ( ! empty( $metadata['editorStyle'] ) ) { + $settings['editor_style'] = register_block_style_handle( + $metadata, + 'editorStyle' + ); + } + + if ( ! empty( $metadata['style'] ) ) { + $settings['style'] = register_block_style_handle( + $metadata, + 'style' + ); + } + + return register_block_type( + $metadata['name'], + array_merge( + $settings, + $args + ) + ); +} + /** * Determine whether a post or content string has blocks. * diff --git a/tests/phpunit/tests/blocks/fixtures/block.asset.php b/tests/phpunit/tests/blocks/fixtures/block.asset.php new file mode 100644 index 0000000000000..792bbb6c84cd3 --- /dev/null +++ b/tests/phpunit/tests/blocks/fixtures/block.asset.php @@ -0,0 +1,6 @@ + array(), + 'version' => 'test', +); diff --git a/tests/phpunit/tests/blocks/fixtures/block.css b/tests/phpunit/tests/blocks/fixtures/block.css new file mode 100644 index 0000000000000..5bbe1134f7048 --- /dev/null +++ b/tests/phpunit/tests/blocks/fixtures/block.css @@ -0,0 +1 @@ +/* Test CSS file */ diff --git a/tests/phpunit/tests/blocks/fixtures/block.js b/tests/phpunit/tests/blocks/fixtures/block.js new file mode 100644 index 0000000000000..0bdf0f5ad91f7 --- /dev/null +++ b/tests/phpunit/tests/blocks/fixtures/block.js @@ -0,0 +1 @@ +/* Test JavaScript file. */ diff --git a/tests/phpunit/tests/blocks/fixtures/block.json b/tests/phpunit/tests/blocks/fixtures/block.json new file mode 100644 index 0000000000000..be4205ce767af --- /dev/null +++ b/tests/phpunit/tests/blocks/fixtures/block.json @@ -0,0 +1,52 @@ +{ + "name": "my-plugin/notice", + "title": "Notice", + "category": "common", + "parent": [ + "core/group" + ], + "providesContext": { + "my-plugin/message": "message" + }, + "usesContext": [ + "groupId" + ], + "icon": "star", + "description": "Shows warning, error or success notices…", + "keywords": [ + "alert", + "message" + ], + "textDomain": "my-plugin", + "attributes": { + "message": { + "type": "string", + "source": "html", + "selector": ".message" + } + }, + "supports": { + "align": true, + "lightBlockWrapper": true + }, + "styles": [ + { + "name": "default", + "label": "Default", + "isDefault": true + }, + { + "name": "other", + "label": "Other" + } + ], + "example": { + "attributes": { + "message": "This is a notice!" + } + }, + "editorScript": "my-plugin-notice-editor-script", + "script": "my-plugin-notice-script", + "editorStyle": "my-plugin-notice-editor-style", + "style": "my-plugin-notice-style" +} diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 8271c9babdf67..514fa807505fa 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -102,6 +102,248 @@ function test_unregister_affects_main_registry() { $this->assertFalse( $registry->is_registered( $name ) ); } + /** + * @ticket 50263 + */ + function test_does_not_remove_block_asset_path_prefix() { + $result = remove_block_asset_path_prefix( 'script-handle' ); + + $this->assertSame( 'script-handle', $result ); + } + + /** + * @ticket 50263 + */ + function test_removes_block_asset_path_prefix() { + $result = remove_block_asset_path_prefix( 'file:./block.js' ); + + $this->assertSame( './block.js', $result ); + } + + /** + * @ticket 50263 + */ + function test_generate_block_asset_handle() { + $block_name = 'unit-tests/my-block'; + + $this->assertSame( + 'unit-tests-my-block-editor-script', + generate_block_asset_handle( $block_name, 'editorScript' ) + ); + $this->assertSame( + 'unit-tests-my-block-script', + generate_block_asset_handle( $block_name, 'script' ) + ); + $this->assertSame( + 'unit-tests-my-block-editor-style', + generate_block_asset_handle( $block_name, 'editorStyle' ) + ); + $this->assertSame( + 'unit-tests-my-block-style', + generate_block_asset_handle( $block_name, 'style' ) + ); + } + + /** + * @ticket 50263 + */ + function test_field_not_found_register_block_script_handle() { + $result = register_block_script_handle( array(), 'script' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 50263 + */ + function test_empty_value_register_block_script_handle() { + $metadata = array( 'script' => '' ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertFalse( $result ); + } + + /** + * @expectedIncorrectUsage register_block_script_handle + * @ticket 50263 + */ + function test_missing_asset_file_register_block_script_handle() { + $metadata = array( + 'file' => __FILE__, + 'name' => 'unit-tests/test-block', + 'script' => 'file:./fixtures/missing-asset.js', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 50263 + */ + function test_handle_passed_register_block_script_handle() { + $metadata = array( + 'editorScript' => 'test-script-handle', + ); + $result = register_block_script_handle( $metadata, 'editorScript' ); + + $this->assertSame( 'test-script-handle', $result ); + } + + /** + * @ticket 50263 + */ + function test_success_register_block_script_handle() { + $metadata = array( + 'file' => __FILE__, + 'name' => 'unit-tests/test-block', + 'script' => 'file:./fixtures/block.js', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertSame( 'unit-tests-test-block-script', $result ); + } + + /** + * @ticket 50263 + */ + function test_field_not_found_register_block_style_handle() { + $result = register_block_style_handle( array(), 'style' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 50263 + */ + function test_empty_value_found_register_block_style_handle() { + $metadata = array( 'style' => '' ); + $result = register_block_style_handle( $metadata, 'style' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 50263 + */ + function test_handle_passed_register_block_style_handle() { + $metadata = array( + 'style' => 'test-style-handle', + ); + $result = register_block_style_handle( $metadata, 'style' ); + + $this->assertSame( 'test-style-handle', $result ); + } + + /** + * @ticket 50263 + */ + function test_success_register_block_style_handle() { + $metadata = array( + 'file' => __FILE__, + 'name' => 'unit-tests/test-block', + 'style' => 'file:./fixtures/block.css', + ); + $result = register_block_style_handle( $metadata, 'style' ); + + $this->assertSame( 'unit-tests-test-block-style', $result ); + } + + /** + * Tests that the function returns false when the `block.json` is not found + * in the WordPress core. + * + * @ticket 50263 + */ + function test_metadata_not_found_in_wordpress_core() { + $result = register_block_type_from_metadata( 'unknown' ); + + $this->assertFalse( $result ); + } + + /** + * Tests that the function returns false when the `block.json` is not found + * in the current directory. + * + * @ticket 50263 + */ + function test_metadata_not_found_in_the_current_directory() { + $result = register_block_type_from_metadata( __DIR__ ); + + $this->assertFalse( $result ); + } + + /** + * Tests that the function returns the registered block when the `block.json` + * is found in the fixtures directory. + * + * @ticket 50263 + */ + function test_block_registers_with_metadata_fixture() { + $result = register_block_type_from_metadata( + __DIR__ . '/fixtures' + ); + + $this->assertInstanceOf( 'WP_Block_Type', $result ); + $this->assertSame( 'my-plugin/notice', $result->name ); + $this->assertSame( 'Notice', $result->title ); + $this->assertSame( 'common', $result->category ); + $this->assertEqualSets( array( 'core/group' ), $result->parent ); + $this->assertSame( 'star', $result->icon ); + $this->assertSame( 'Shows warning, error or success notices…', $result->description ); + $this->assertEqualSets( array( 'alert', 'message' ), $result->keywords ); + $this->assertEquals( + array( + 'message' => array( + 'type' => 'string', + 'source' => 'html', + 'selector' => '.message', + ), + ), + $result->attributes + ); + $this->assertEquals( + array( + 'my-plugin/message' => 'message', + ), + $result->provides_context + ); + $this->assertEqualSets( array( 'groupId' ), $result->uses_context ); + $this->assertEquals( + array( + 'align' => true, + 'lightBlockWrapper' => true, + ), + $result->supports + ); + $this->assertEquals( + array( + array( + 'name' => 'default', + 'label' => 'Default', + 'isDefault' => true, + ), + array( + 'name' => 'other', + 'label' => 'Other', + ), + ), + $result->styles + ); + $this->assertEquals( + array( + 'attributes' => array( + 'message' => 'This is a notice!', + ), + ), + $result->example + ); + $this->assertSame( 'my-plugin-notice-editor-script', $result->editor_script ); + $this->assertSame( 'my-plugin-notice-script', $result->script ); + $this->assertSame( 'my-plugin-notice-editor-style', $result->editor_style ); + $this->assertSame( 'my-plugin-notice-style', $result->style ); + } + /** * @ticket 45109 */