Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype: Server-side block attributes sourcing #18414

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function gutenberg_menu() {
function gutenberg_wordpress_version_notice() {
echo '<div class="error"><p>';
/* 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 '</p></div>';

deactivate_plugins( array( 'gutenberg/gutenberg.php' ) );
Expand Down Expand Up @@ -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', '<' ) ) {
Soean marked this conversation as resolved.
Show resolved Hide resolved
add_action( 'admin_notices', 'gutenberg_wordpress_version_notice' );
return;
}
Expand Down
39 changes: 39 additions & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have block.json for all core blocks, those which are dynamic don't have this metadata file provided because we still didn't resolve the following issues:

Those aren't blockers for this proposal if we were to use only attributes though. So maybe it would be a good idea to move attributes to the block.json file to better promote this format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still didn't resolve the following issues:

You'll have to forgive me since it's been a while that I've revisited some of those specific details of the JSON manifest. Working through this prototype forced me to consider how we would implement at least some of those supports on the server (className, align, anchor are implemented in this pull request).

For translations, I seem to recall something about how we considered to wrap the translateable fields via __ et. al., automatically? I'm not sure exactly how we determine the domain in that case.

As a prototype, I'm also fine to start splitting those off into their own individual tasks. It was at least interesting to explore the feasibility of pulling them in and highlighting some of these shortcomings (notably supports).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll have to forgive me since it's been a while that I've revisited some of those specific details of the JSON manifest. Working through this prototype forced me to consider how we would implement at least some of those supports on the server (className, align, anchor are implemented in this pull request).

Nice, I missed that, sorry about it :(

For translations, I seem to recall something about how we considered to wrap the translateable fields via __ et. al., automatically? I'm not sure exactly how we determine the domain in that case.

There needs to be the textDomain field declared in the block.json file. You can check my prototype for JS side as a reference: #16088.

I don't think it is a concern though in the context of attributes. I just wanted to raise awareness of that. The general agreement was that attributes shouldn't be translatable.

As a prototype, I'm also fine to start splitting those off into their own individual tasks. It was at least interesting to explore the feasibility of pulling them in and highlighting some of these shortcomings (notably supports).

Yes, supports seems like the only place which can cause issues for the proposed code.

foreach ( $block_manifests as $block_manifest ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should some level of validation be happening here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should some level of validation be happening here?

Do you mean that it has necessary properties to consider it a valid block manifest?

There are some simple checks below, both to account that the file could be parsed as JSON, and that it has a name. We could expand on this.

if ( is_null( $block_settings ) || ! isset( $block_settings['name'] ) ) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation of types, like it string, int, array etc?

Copy link
Member Author

@aduth aduth Nov 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation of types, like it string, int, array etc?

If I understand you correctly, we probably should want something like what exists in WP_Block_Type#prepare_attributes_for_render, though I expect this would be applied at the time that $attributes are being sourced.

$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.
Expand Down Expand Up @@ -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' );

Expand Down
200 changes: 200 additions & 0 deletions lib/class-wp-sourced-attributes-block-parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php
/**
* Block Serialization Parser
*
* @package Gutenberg
*/

/**
* Class WP_Sourced_Attributes_Block_Parser
*
* Parses a document and constructs a list of parsed block objects
*
* @since 6.9.0
*/
class WP_Sourced_Attributes_Block_Parser extends WP_Block_Parser {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests?


/**
* Parses a document and returns a list of block structures
*
* When encountering an invalid parse will return a best-effort
* parse. In contrast to the specification parser this does not
* return an error on invalid inputs.
*
* @since 6.9.0
*
* @param string $document Input document being parsed.
* @param WP_Block_Type_Registry $registry Block type registry from which
* block attributes schema can be
* retrieved.
* @param int|null $post_id Optional post ID.
* @return WP_Block_Parser_Block[]
*/
function parse( $document, $registry = null, $post_id = null ) {
if ( is_null( $registry ) ) {
$registry = WP_Block_Type_Registry::get_instance();
}

$blocks = parent::parse( $document );

foreach ( $blocks as $i => $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 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is going to be expensive from a compute level. Is there anyway we can cache the result?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is going to be expensive from a compute level. Is there anyway we can cache the result?

I'd thought a bit about what might make sense to cache. Since the HTML of each block will likely be unique, I don't know that we would want to cache either the loaded HTML or the queried results, as the cache hit ratio would be very low.

What might make sense, depending on whether it makes a measurable difference:

  • Caching the $document itself.
    • If constructing DOMDocument is expensive (I don't know that it is)
  • Caching the converted XPath selectors
    • The conversion may or may not be expensive (might also depend which implementation we choose), but there's a higher likelihood we would reuse those on a a per-block-type basis (e.g. every paragraph will be running the p selector).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about cache the attributes, in say post meta?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about cache the attributes, in say post meta?

Hm, I'd have to think about it more, but that does seem like a good idea. In fact, it might then make sense to run this sourcing logic when a post is saved, rather than at parse-time (the parse would just read the cached result).

$document = new DOMDocument();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this class has some requirements. Can we confirm that the libxml package is currently a required one for WP Core?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this class has some requirements. Can we confirm that the libxml package is currently a required one for WP Core?

From the page you link, it says "libxml is enabled by default".

We could still have some graceful fallback here for environments where it's explicitly disabled, although it would be unable to populate $attributes, yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to change the requires for WP core.

try {
// loadHTML may log warnings for unexpected markup.
// phpcs:ignore
@$document->loadHTML( '<html><body>' . $block['innerHTML'] . '</body></html>' );
} catch ( Exception $e ) {
return null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I have worked with DOMDocument I found it extremely helpful to extract the setup code into a separate function that abstracts away the noisy quirks.

$document = parseHTML( $block['innerHTML'] );
if ( null === $document ) {
	return null;
}

it's a small thing to abstract but I find in my experience it worth it especially as we learn about the settings we need to activate with whitespace and with parse-handling.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I have worked with DOMDocument I found it extremely helpful to extract the setup code into a separate function that abstracts away the noisy quirks.

That seems like a reasonable revision, for sure! I expect it would also make writing the tests a little nicer, since I'd not need to lump all the error cases for this function otherwise intended specifically at sourcing values.


$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;
}

}
3 changes: 2 additions & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading