From 71ea6597c88726797bf5771bb1b84ef2a13ae8c1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:04:30 -0500 Subject: [PATCH 1/5] Framework: Bump minimum required WordPress to 5.2 --- gutenberg.php | 4 ++-- phpcs.xml.dist | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 76bf483c8f74b..dd2c9860ac99c 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -87,7 +87,7 @@ function gutenberg_menu() { function gutenberg_wordpress_version_notice() { echo '

'; /* translators: %s: Minimum required version */ - printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.0.0' ); + printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.2.0' ); echo '

'; deactivate_plugins( array( 'gutenberg/gutenberg.php' ) ); @@ -122,7 +122,7 @@ function gutenberg_pre_init() { // Strip '-src' from the version string. Messes up version_compare(). $version = str_replace( '-src', '', $wp_version ); - if ( version_compare( $version, '5.0.0', '<' ) ) { + if ( version_compare( $version, '5.2.0', '<' ) ) { add_action( 'admin_notices', 'gutenberg_wordpress_version_notice' ); return; } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index e039c8724c948..87c4674f45e6e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -3,7 +3,7 @@ Sniffs for WordPress plugins, with minor modifications for Gutenberg - + From c1e7d7edb39ec78a448b42bc055b82d46c19734b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:07:09 -0500 Subject: [PATCH 2/5] Parser: Override parser to implement attributes sourcing --- ...ass-wp-sourced-attributes-block-parser.php | 198 ++++++++++++++++++ lib/load.php | 3 +- lib/parser.php | 117 +++++++++++ 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 lib/class-wp-sourced-attributes-block-parser.php create mode 100644 lib/parser.php diff --git a/lib/class-wp-sourced-attributes-block-parser.php b/lib/class-wp-sourced-attributes-block-parser.php new file mode 100644 index 0000000000000..97f6161b93e30 --- /dev/null +++ b/lib/class-wp-sourced-attributes-block-parser.php @@ -0,0 +1,198 @@ + $block ) { + $block_type = $registry->get_registered( $block['blockName'] ); + if ( is_null( $block_type ) || ! isset( $block_type->attributes ) ) { + continue; + } + + $sourced_attributes = $this->get_sourced_attributes( + $block, + $block_type->attributes, + $post_id + ); + + $blocks[ $i ]['attrs'] = array_merge( $block['attrs'], $sourced_attributes ); + } + + return $blocks; + } + + /** + * Returns an array of sourced attribute values for a block. + * + * @param WP_Block_Parser_Block $block Parsed block object. + * @param array $attributes_schema Attributes of registered + * block type for block. + * @param int|null $post_id Optional post ID. + * @return array Sourced attribute values. + */ + function get_sourced_attributes( $block, $attributes_schema, $post_id ) { + $attributes = array(); + + foreach ( $attributes_schema as $key => $attribute_schema ) { + if ( isset( $attribute_schema['source'] ) ) { + $attributes[ $key ] = $this->get_sourced_attribute( + $block, + $attribute_schema, + $post_id + ); + } + } + + return $attributes; + } + + /** + * Returns a sourced attribute value for a block, for attribute type which + * sources from HTML. + * + * @param WP_Block_Parser_Block $block Parsed block object. + * @param array $attribute_schema Attribute schema for + * individual attribute to + * be parsed. + * @return mixed Sourced attribute value. + */ + function get_html_sourced_attribute( $block, $attribute_schema ) { + $document = new DOMDocument(); + try { + $document->loadHTML( '' . $block['innerHTML'] . '' ); + } catch ( Exception $e ) { + return null; + } + + $selector = 'body'; + if ( isset( $attribute_schema['selector'] ) ) { + $selector .= ' ' . $attribute_schema['selector']; + } + + $xpath_selector = _wp_css_selector_to_xpath( $selector ); + $xpath = new DOMXpath( $document ); + $match = $xpath->evaluate( $xpath_selector ); + + if ( 0 === $match->count() ) { + return null; + } + + $element = $match->item( 0 ); + + switch ( $attribute_schema['source'] ) { + case 'text': + /* + * See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 + */ + // phpcs:ignore + return $element->textContent; + + case 'html': + $inner_html = ''; + + /* + * See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 + */ + // phpcs:ignore + foreach ( $element->childNodes as $child ) { + /* + * See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 + */ + // phpcs:ignore + $inner_html .= $child->ownerDocument->saveXML( $child ); + } + + return $inner_html; + + case 'attribute': + if ( ! isset( $attribute_schema['attribute'] ) || + is_null( $element->attributes ) ) { + return null; + } + + $attribute = $element->attributes->getNamedItem( $attribute_schema['attribute'] ); + + /* + * See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 + */ + // phpcs:ignore + return is_null( $attribute ) ? null : $attribute->nodeValue; + } + + return null; + } + + /** + * Returns a sourced attribute value for a block. + * + * @param WP_Block_Parser_Block $block Parsed block object. + * @param array $attribute_schema Attribute schema for + * individual attribute to + * be parsed. + * @param int|null $post_id Optional post ID. + * @return mixed Sourced attribute value. + */ + function get_sourced_attribute( $block, $attribute_schema, $post_id ) { + switch ( $attribute_schema['source'] ) { + case 'text': + case 'html': + case 'attribute': + return $this->get_html_sourced_attribute( $block, $attribute_schema ); + + case 'query': + // TODO: Implement. + return null; + + case 'property': + case 'node': + case 'children': + case 'tag': + // Unsupported or deprecated. + return null; + + case 'meta': + if ( ! is_null( $post_id ) && isset( $attribute_schema['meta'] ) ) { + return get_post_meta( $post_id, $attribute_schema['meta'] ); + } + + return null; + } + + return null; + } + +} diff --git a/lib/load.php b/lib/load.php index 58e545b38a6f7..54cfe25350a1e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -51,7 +51,8 @@ function gutenberg_is_experiment_enabled( $name ) { } require dirname( __FILE__ ) . '/compat.php'; - +require dirname( __FILE__ ) . '/class-wp-sourced-attributes-block-parser.php'; +require dirname( __FILE__ ) . '/parser.php'; require dirname( __FILE__ ) . '/blocks.php'; require dirname( __FILE__ ) . '/templates.php'; require dirname( __FILE__ ) . '/template-loader.php'; diff --git a/lib/parser.php b/lib/parser.php new file mode 100644 index 0000000000000..5c9cfb0fde29c --- /dev/null +++ b/lib/parser.php @@ -0,0 +1,117 @@ + + */ + + // Remove spaces around operators. + $selector = preg_replace( '/\s*>\s*/', '>', $selector ); + $selector = preg_replace( '/\s*~\s*/', '~', $selector ); + $selector = preg_replace( '/\s*\+\s*/', '+', $selector ); + $selector = preg_replace( '/\s*,\s*/', ',', $selector ); + $selectors = preg_split( '/\s+(?![^\[]+\])/', $selector ); + + foreach ( $selectors as &$selector ) { + /* , */ + $selector = preg_replace( '/,/', '|descendant-or-self::', $selector ); + /* input:checked, :disabled, etc. */ + $selector = preg_replace( '/(.+)?:(checked|disabled|required|autofocus)/', '\1[@\2="\2"]', $selector ); + /* input:autocomplete, :autocomplete */ + $selector = preg_replace( '/(.+)?:(autocomplete)/', '\1[@\2="on"]', $selector ); + /* input:button, input:submit, etc. */ + $selector = preg_replace( '/:(text|password|checkbox|radio|button|submit|reset|file|hidden|image|datetime|datetime-local|date|month|time|week|number|range|email|url|search|tel|color)/', 'input[@type="\1"]', $selector ); + /* foo[id] */ + $selector = preg_replace( '/(\w+)\[([_\w-]+[_\w\d-]*)\]/', '\1[@\2]', $selector ); + /* [id] */ + $selector = preg_replace( '/\[([_\w-]+[_\w\d-]*)\]/', '*[@\1]', $selector ); + /* foo[id=foo] */ + $selector = preg_replace( '/\[([_\w-]+[_\w\d-]*)=[\'"]?(.*?)[\'"]?\]/', '[@\1="\2"]', $selector ); + /* [id=foo] */ + $selector = preg_replace( '/^\[/', '*[', $selector ); + /* div#foo */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*)\#([_\w-]+[_\w\d-]*)/', '\1[@id="\2"]', $selector ); + /* #foo */ + $selector = preg_replace( '/\#([_\w-]+[_\w\d-]*)/', '*[@id="\1"]', $selector ); + /* div.foo */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*)\.([_\w-]+[_\w\d-]*)/', '\1[contains(concat(" ",@class," ")," \2 ")]', $selector ); + /* .foo */ + $selector = preg_replace( '/\.([_\w-]+[_\w\d-]*)/', '*[contains(concat(" ",@class," ")," \1 ")]', $selector ); + /* div:first-child */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*):first-child/', '*/\1[position()=1]', $selector ); + /* div:last-child */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*):last-child/', '*/\1[position()=last()]', $selector ); + /* :first-child */ + $selector = str_replace( ':first-child', '*/*[position()=1]', $selector ); + /* :last-child */ + $selector = str_replace( ':last-child', '*/*[position()=last()]', $selector ); + /* :nth-last-child */ + $selector = preg_replace( '/:nth-last-child\((\d+)\)/', '[position()=(last() - (\1 - 1))]', $selector ); + /* div:nth-child */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*):nth-child\((\d+)\)/', '*/*[position()=\2 and self::\1]', $selector ); + /* :nth-child */ + $selector = preg_replace( '/:nth-child\((\d+)\)/', '*/*[position()=\1]', $selector ); + /* :contains(Foo) */ + $selector = preg_replace( '/([_\w-]+[_\w\d-]*):contains\((.*?)\)/', '\1[contains(string(.),"\2")]', $selector ); + /* > */ + $selector = preg_replace( '/>/', '/', $selector ); + /* ~ */ + $selector = preg_replace( '/~/', '/following-sibling::', $selector ); + /* + */ + $selector = preg_replace( '/\+([_\w-]+[_\w\d-]*)/', '/following-sibling::\1[position()=1]', $selector ); + $selector = str_replace( ']*', ']', $selector ); + $selector = str_replace( ']/*', ']', $selector ); + } + + // ' ' + $selector = implode( '/descendant::', $selectors ); + $selector = 'descendant-or-self::' . $selector; + // :scope + $selector = preg_replace( '/(((\|)?descendant-or-self::):scope)/', '.\3', $selector ); + // $element + $sub_selectors = explode( ',', $selector ); + + foreach ( $sub_selectors as $key => $sub_selector ) { + $parts = explode( '$', $sub_selector ); + $sub_selector = array_shift( $parts ); + + if ( count( $parts ) && preg_match_all( '/((?:[^\/]*\/?\/?)|$)/', $parts[0], $matches ) ) { + $results = $matches[0]; + $results[] = str_repeat( '/..', count( $results ) - 2 ); + $sub_selector .= implode( '', $results ); + } + + $sub_selectors[ $key ] = $sub_selector; + } + + $selector = implode( ',', $sub_selectors ); + + return $selector; +} From c7cded60c73e8e5c784743bc3474a0914c55928e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:07:33 -0500 Subject: [PATCH 3/5] Blocks: Extend registered block types to include default attributes --- lib/blocks.php | 39 +++++++++++++++++++++++++++++++++++++++ lib/parser.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/lib/blocks.php b/lib/blocks.php index 1dc7277d85ee8..dc9e6a656057f 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -27,6 +27,39 @@ function gutenberg_get_registered_social_link_blocks() { return $registered_social_link_blocks; } +/** + * Registers blocks using block manifests discovered in block library. + * + * @since 6.9.0 + */ +function gutenberg_register_block_types() { + $registry = WP_Block_Type_Registry::get_instance(); + + $block_manifests = glob( dirname( dirname( __FILE__ ) ) . '/packages/block-library/src/*/block.json' ); + foreach ( $block_manifests as $block_manifest ) { + $block_settings = json_decode( file_get_contents( $block_manifest ), true ); + if ( is_null( $block_settings ) || ! isset( $block_settings['name'] ) ) { + continue; + } + + if ( $registry->is_registered( $block_settings['name'] ) ) { + $block_settings = array_merge( + (array) $registry->get_registered( $block_settings['name'] ), + $block_settings + ); + + $registry->unregister( $block_settings['name'] ); + } + + register_block_type( + $block_settings['name'], + // Apply default attributes manually, as it isn't currently possible + // to filter block registration. + gutenberg_add_default_attributes( $block_settings ) + ); + } +} + /** * Substitutes the implementation of a core-registered block type, if exists, * with the built result from the plugin. @@ -76,6 +109,12 @@ function gutenberg_reregister_core_block_types() { require $blocks_dir . $file; } + + // Add block library registration only after this is reached, since the + // above `require` calls may attach their own `init` actions which are + // deferred to run at the latest priority. Thus, to correctly merge block + // settings from manifests, it must be the last to run. + add_action( 'init', 'gutenberg_register_block_types', 20 ); } add_action( 'init', 'gutenberg_reregister_core_block_types' ); diff --git a/lib/parser.php b/lib/parser.php index 5c9cfb0fde29c..1b00a89b23a22 100644 --- a/lib/parser.php +++ b/lib/parser.php @@ -16,6 +16,50 @@ function gutenberg_replace_block_parser_class() { } add_filter( 'block_parser_class', 'gutenberg_replace_block_parser_class' ); +/** + * Given a registered block type settings array, assigns default attributes. + * This must be called manually, as there is currently no way to hook to block + * registration. + * + * @since 6.9.0 + * + * @param array $block_settings Block settings. + * @return array Block settings with default attributes. + */ +function gutenberg_add_default_attributes( $block_settings ) { + $supports = isset( $block_settings['supports'] ) ? $block_settings['supports'] : array(); + $attributes = isset( $block_settings['attributes'] ) ? $block_settings['attributes'] : array(); + + if ( ! empty( $supports['align'] ) ) { + if ( ! isset( $attributes['align'] ) ) { + $attributes['align'] = array(); + } + + $attributes['align']['type'] = 'string'; + } + + if ( ! empty( $supports['anchor'] ) ) { + if ( ! isset( $attributes['anchor'] ) ) { + $attributes['anchor'] = array(); + } + + $attributes['anchor']['type'] = 'string'; + $attributes['anchor']['source'] = 'attribute'; + $attributes['anchor']['attribute'] = 'id'; + $attributes['anchor']['selector'] = '*'; + } + + if ( ! isset( $supports['customClassName'] ) || false !== $supports['customClassName'] ) { + if ( ! isset( $attributes['className'] ) ) { + $attributes['className'] = array(); + } + + $attributes['className']['type'] = 'string'; + } + + return $block_settings; +} + /** * Given a CSS selector string, returns an equivalent XPath selector. * From aafc34d1a5109a65e350ce67b6c696946a574735 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:07:52 -0500 Subject: [PATCH 4/5] REST API: Include parsed blocks in REST Posts response --- lib/rest-api.php | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/lib/rest-api.php b/lib/rest-api.php index 6ad5e6d0e6f0e..1fed2193fbf52 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -52,7 +52,74 @@ function gutenberg_filter_oembed_result( $response, $handler, $request ) { } add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 ); +/** + * Reducer callback operating on an array of block results for REST response. + * Given a block entity, adds an array formatted for use in the response, if + * the block is named. + * + * @param array $result Blocks to include. + * @param array $block The parsed block to format. + * @return array Block formatted for REST response. + */ +function gutenberg_format_block_for_rest( $result, $block ) { + if ( ! empty( $block['blockName'] ) ) { + $result[] = array( + 'name' => $block['blockName'], + 'attributes' => (object) $block['attrs'], + 'inner_blocks' => array_reduce( + $block['innerBlocks'], + 'gutenberg_format_block_for_rest', + array() + ), + ); + } + + return $result; +} +/** + * Augments REST Posts controller response to include blocks data, only if + * response already includes `content.raw`. + * + * In a more correct implementation, this would use item schema as basis for + * whether `content.blocks` is included in a response. + * + * @since 6.9.0 + * + * @param WP_REST_Response $response The response object. + * @return WP_HTTP_Response The filtered response object. + */ +function gutenberg_add_blocks_to_rest_posts_content( $response ) { + if ( ! empty( $response->data['content']['raw'] ) ) { + $response->data['content']['blocks'] = array_reduce( + parse_blocks( $response->data['content']['raw'] ), + 'gutenberg_format_block_for_rest', + array() + ); + } + + return $response; +} + +/** + * Adds filter for all registered post types to augment REST API responses for + * that post type to include blocks data. + * + * In a core implementation, this would not be necessary, as the logic for + * including blocks data would be part of the controller implementation. + * + * @since 6.9.0 + */ +function gutenberg_filter_rest_prepare_post_types() { + $post_types = get_post_types(); + foreach ( $post_types as $post_type ) { + add_filter( + 'rest_prepare_' . $post_type, + 'gutenberg_add_blocks_to_rest_posts_content' + ); + } +} +add_action( 'rest_pre_dispatch', 'gutenberg_filter_rest_prepare_post_types' ); /** * Start: Include for phase 2 From 1e359a8d467b23c33c664f6c277c2549866be2dd Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:14:23 -0500 Subject: [PATCH 5/5] Parser: Suppress DOMDocument parse warnings --- lib/class-wp-sourced-attributes-block-parser.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-sourced-attributes-block-parser.php b/lib/class-wp-sourced-attributes-block-parser.php index 97f6161b93e30..bb8a852d1384f 100644 --- a/lib/class-wp-sourced-attributes-block-parser.php +++ b/lib/class-wp-sourced-attributes-block-parser.php @@ -93,7 +93,9 @@ function get_sourced_attributes( $block, $attributes_schema, $post_id ) { function get_html_sourced_attribute( $block, $attribute_schema ) { $document = new DOMDocument(); try { - $document->loadHTML( '' . $block['innerHTML'] . '' ); + // loadHTML may log warnings for unexpected markup. + // phpcs:ignore + @$document->loadHTML( '' . $block['innerHTML'] . '' ); } catch ( Exception $e ) { return null; }